Открыть список
Как стать автором
Обновить
2988,18
Рейтинг
RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Две недели с F#

Блог компании RUVDS.comПрограммированиеF#
А вы когда-нибудь записывали свои впечатления от изучения нового языка? Записывали все, что вам не понравилось, чтобы через пару недель изучения понять, насколько недальновидными и тупыми они были? 



На днях я понял F#, и попытаюсь описать словами мысль, стоящую за языком. 

Почему ты не Powershell?


Первым делом, как только уселся за F#, ознакомившись со стайл гайдом, начал переносить команды из Powershell, которые использую чаще всего. В языке есть пайп оператор, ну, можно программировать как на Powershell. Да?

Вот сейчас возьму и функционально получу корневую директорию файла, не используя богомерзкие классы классы из .net.

Все очень просто, берем путь, преобразовываем строку в массив помощью split, по System.IO.Path.DirectorySeparatorChar, берем последний элемент из массива и делаем .trim.

Да, на F# есть весь .net а в .net всё есть, но я не за этим сел. Вот так этот велосипед выглядит на Powershell:

$Path = «C:\users\test\folder»
$Trimer = $Path.Split(«\»)[$Path.Split(«\»).Count - 1]
$Path.Trim($Trimer)

В этом коде много проблем, он просто ужасен, но именно его я и буду переписывать.

Сейчас просто перепишу, ну что может пойти не так?

▍Не такой уж и умный компилятор


let splitPath inputObject: string =
    let q = inputObject.Split(System.IO.Path.DirectorySeparatorChar) 
    q

Написав две строки кода, сразу получаю ошибку:

FS0072 Поиск объекта неопределенного типа, основанного на информации до данной точки программы. Возможно, перед данной точкой программы потребуется аннотация типа, с целью ограничения типа объекта. Возможно, это позволит разрешить поиск.

На разборку у меня ушло минут 30, я не мог поверить, оказывается, компилятор не смог определить, что имеет дело со строкой из типа входного объекта. 

let splitPath inputObject: string =
    let mutable inputObject : string = inputObject
    let q = inputObject.Split(System.IO.Path.DirectorySeparatorChar) 
    q

Пришлось задавать типы прямо внутри функции лишний раз копируя входные данные. 

▍LInq


Компилятор не делает всю работу за меня – ну и ладно. Такие сложности не остановят меня от написания своего собственного костыля.

Часть моей гениальной задумки лежала на Linq, на Trim и Last. Но Trim не работает со string, он работает c Char, то есть нужно переворачивать последний элемент листа и откусывать от строки по символу. 

▍Нет ++


Linq работает не так, как я хочу – ну и не надо. Я посчитаю количество элементов в массиве и выберу нужный, а потом переверну его, разобью на char[] и обрежу таки стрингу!

Но как оказалось, не посчитаю, даже в мутабельной переменной нельзя без сильной головной боли сделать простой счетчик. Сделать то можно, но неудобно.

На этом месте я понял, что совсем ничего не понимаю и начал изучать язык.

F#, ну зачем?


А изучение языка я начал с просмотра чужого кода и лекций от крутых мужиков.

▍Printf, printfn, нейминг


Это покоробило меня еще в самом начале, функция printf выводит символы в той же строке, а printfn в новой строке. В этом весь F#.

Меня, как человека знакомого с концепцией функционального программирования из Powershell это покоробило, после Powershell’a любой другой язык кажется каким-то куцым.

Если бы я делал F#, я бы сделал какую-то такую функцию:

Out-Host «Input string» -Newline

Подход божественного павершелла к аргументам лучший, ибо не нужно держать в голове порядок указания параметров.

▍Napespaces и ленивый Open


Сразу после своего собственного костыля я попытался сделать сайт на основе шаблона ASP NET MVC. К сожалению, из MVC там только С, но контроллеры действительно получаются очень красивые и компактные.

В F# все файлы в F# ведут себя как скрипты. Переменная или функция не объявленные выше не могут использоваться ниже.

С помощью директивы Open мы открываем неймспейсы и модули. Это аналог Using и Import-Module. По аналогии с Powershell, я могу импортнуть файл в котором есть коллекция со всеми её функциями, вставить её в середину файла и все заработает прям как в павершелле? Нет.

Если в F# файл, его мало прочитать, нужно, чтобы хотя бы одна, причем любая функция из этого файла была вызвана.

▍||>,  <|, почему не | ?


Оператор |> нужен чтобы передавать значение в функцию.

||> существует чтобы передавать кортежи в функцию.

|||> а этот монстр передает кортеж из трёх в функцию. 

Работа с кортежами выглядит так:

(1, 2) ||> someFunction

А с единичной переменной вот так:

1 |> someFunction

Тут неясно, что мешает компилятору определить с чем он имеет дело, кортежи явно указываются как кортежи и две палки со стрелочкой рядом с ними выглядят избыточно.

Эта ошибка была совершена из-за другой ошибки, <| — Pipe back оператора. Он был введен, чтобы при композиции в некоторых случаях можно было избавиться от скобочек. К примеру это:

printfn «%s« (string «Value»)

Можно написать так:

printfn «%s« <| string «Value»

Дон Сайм, архитектор языка как раз говорил об этом тут.

Почему ты не F#?


Помните о лекциях от крутых мужиков? Я прослушал лекцию от Скотта Влашина и на этом моменте меня пробило, я понял эту гигантскую мысль, осознание накрыло со всех сторон, это совсем другая парадигма. Я мог только сидеть на стуле и ухать.

▍Some, None


Скажем, мы пытаемся прочитать файл на двух языках. В F# и С#. К примеру, пытаемся прочитать txt файл и что-то сделать с его содержимым. Если что-то пойдет не так, код написанный на C# упадет сразу в двух местах, потому что StreamReader не может прочитать файл, которого нет, да и обработчик не умеет работать с нулём.

Вся задумка состоит в том, что даже если мы не возвращаем Value, мы всегда возвращаем что-то, у нас есть тип, у нас есть Value of None. И если мы не получили Some of Value, то получили None.

Как пример, работа с дотнетовскими коллекциями в F#:

  let dictionary = Dictionary<string, string> ()
   
    let getFromDictionary key =
        match dictionary.TryGetValue (key) with
        | true, value -> Some (value)
        | false, _ -> None

Кстати, этот же метод можно реализовать и на C# с помощью расширений, например для этого есть LanguageExt.Core и Maybe монады, но на C# все это выглядит просто ужасно.

▍Discriminated union aka алгебраические типы


Чтобы прочитать файл на C# мы должны писать защитный код как минимум в 2 местах. Сначала мы должны проверить, что файл существует и что файл соответствует формату, чтобы не упал streamreader.

Чтобы не падал наш процессор, нужно проверить, что файл не пустой и что он тоже правильного формата. Это абсолютно легитимный способ писать код на C#, но не на F#. 

К примеру, возьмем пример, где наша программа может работать только с txt и ini файлами.

type ValidInput =
    | Txt of string
    | Ini of string
 
type InvalidInput =
    | WrongFormat
    | FileDoesNotExists
    | FileIsEmpty
    | OtherBadFile
 
type Input =
    | ValidInput of ValidInput
    | InvalidInput of InvalidInput

На F# защитный код пишется только в самом начале. Благодаря мощной системе типов и паттерн матчингу мы можем хендлить все варианты развития событий, не смешивая защитную логику с остальной.

let input = testInputObject request
 
match input with
| ValidInput (x) -> invokeAction x 
| InvalidInput (x) -> writeReject x 

▍Непробиваемый дизайн языка


Непробиваемый ни нулями, ни багами. И гениальность состоит из нескольких компонентов:

  • Нет return. Вернуть значение из функции можно только в конце после отработки всей логики.
  • Нет if без else. Потому, что if без else обычно применяется там, где будет возвращен null.
  • Type of Value. В F# всегда возвращается либо тип, либо значение какого-то типа, но никогда не Null.

Всего 3 принципа которые даже я понял. Всего три принципа были нужны, чтобы отлавливать баги на стадии компиляции.

▍Вся область проектирование перед глазами


Это вытекает из особенности языка, все файлы в F# ведут себя как скрипты. Переменная или функция не объявленные выше не могут использоваться ниже.
  
Что с одной стороны, это не дает писать код в вольной спагетти манере, но с другой, становится ясно, куда смотреть. Если функция используется ниже, то она объявлена выше.

Особенно прекрасно это смотрится на бизнес-логике связанной с ASP .NET. Все типы и все функции, связанные с определенной страницей на сайте – все на одном листе. 

▍Имутабельность по умолчанию


На первый взгляд может показаться, что это просто оверхед по памяти и работа для сборщика мусора. Но как же приятно отлаживать код, когда знаешь, где и какая переменная изменилась.
Ты знаешь, что может меняться потому, что сам указал на то, что может меняться. 

Так же я заметил, что чаще всего работаю с коллекциями, стрингбилдерами и т.п., зачем мне мутабельные ссылки на них?

▍DDD


Domain Driven Design в F# это абсолютно нативная вещь и пожалуй, лучший способ разработки. Если вы начнете писать на F#, то сможете и не заподозрить, что начали так делать.

Скажем, мы храним в базе данных данные о пользователях, где часть из них кошки, а другая – попугаи и нам нужно понять, с кем мы имеем дело. У пользователя есть поле с его ID и булёвое поле «HaveWings».

То вот это не F# и не DDD:

let getUserType key =
    let user = getFromDatabase key
    if user.HaveWings = true then "Parrot"
    else "Cat"

В этом случае мы не используем паттерн матчинг, что делает его нерасширяемым и мы не используем типы, поэтому компилятор нам больше не помощник.

А это уже и F# и DDD:


type UserType  =
   | Parrot of User
   | Cat of User
let getUserType key =
    match getFromDatabase key with
    | _, true -> Parrot
    | _, false -> Cat

Тут мы обозначили алгебраические типы, и теперь в случае расширения кода, при добавлении еще одного типа пользователей, компилятор напомнит нам о всех местах, где нужно отхендлить новый тип.

Плюс мы используем паттерн матчинг, что в будущем, когда модель данных станет сложнее, позволит нам избавиться от вложенных else if и длинных свитчей.

В целом, можно программировать на F# и без DDD, но если можно сделать код человекопонятным, вот почему бы и нет? 



Все то время, что я не знал, что пытался писать на смеси C# и Powershell даже не понимая того, что в F# то, как ты пишешь код так же важно, как и соблюдать синтаксис.

Я понял в чем суть имутабельности по дефолту, я понял DDD, я понял, в чем главная задумка языка.

Так я полюбил F# и мне больше не бомбит.

Скрытый текст
Монстр-велосипед был добавлен в статью в юмористических целях, но я таки его доделал. И в нем не меньше (если не больше) проблем, чем в коде выше, но тем не менее. 

Если знаете, как сделать его еще лучше — свисните.

let splitPath inputObject = 
    let mutable inputObject : string = inputObject
    let stringArray = inputObject.Split(System.IO.Path.DirectorySeparatorChar)
   
    let mutable outString = ""
    for i in stringArray do
        outString <- i 
 
    let chararray = outString |> Seq.toList |> List.rev
    for c in chararray do
        inputObject <- inputObject.TrimEnd(c)
 
    printfn "%s" inputObject
 
splitPath @"C:\users\test\folder"


Теги:F#ruvds_статьиязыки программированияязык F#
Хабы: Блог компании RUVDS.com Программирование F#
Всего голосов 57: ↑45 и ↓12 +33
Просмотры9.8K

Комментарии 21

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
ruvds.com
Численность
11–30 человек
Дата регистрации
Представитель
ruvds

Блог на Хабре