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

vvoeyksgewqqslge4ny49da8_3g.png

А вы когда-нибудь записывали свои впечатления от изучения нового языка? Записывали все, что вам не понравилось, чтобы через пару недель изучения понять, насколько недальновидными и тупыми они были?  

КАРТИНКА ДО КАТА

На днях я понял 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 ()
   
    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:

let getUserType key =
    let user = getFromDatabase key
    if user.HaveWings = true then "Parrot"
    else "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"

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru