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

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"

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/545740/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *