Две недели с 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 ()
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"