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

На днях я понял F#, и попытаюсь описать словами мысль, стоящую за языком.
Первым делом, как только уселся за F#, ознакомившись со стайл гайдом, начал переносить команды из Powershell, которые использую чаще всего. В языке есть пайп оператор, ну, можно программировать как на Powershell. Да?
Вот сейчас возьму и функционально получу корневую директорию файла, не используя богомерзкие классы классы из .net.
Все очень просто, берем путь, преобразовываем строку в массив помощью split, по System.IO.Path.DirectorySeparatorChar, берем последний элемент из массива и делаем .trim.
Да, на F# есть весь .net а в .net всё есть, но я не за этим сел. Вот так этот велосипед выглядит на Powershell:
В этом коде много проблем, он просто ужасен, но именно его я и буду переписывать.
Сейчас просто перепишу, ну что может пойти не так?
Написав две строки кода, сразу получаю ошибку:
FS0072 Поиск объекта неопределенного типа, основанного на информации до данной точки программы. Возможно, перед данной точкой программы потребуется аннотация типа, с целью ограничения типа объекта. Возможно, это позволит разрешить поиск.
На разборку у меня ушло минут 30, я не мог поверить, оказывается, компилятор не смог определить, что имеет дело со строкой из типа входного объекта.
Пришлось задавать типы прямо внутри функции лишний раз копируя входные данные.
Компилятор не делает всю работу за меня – ну и ладно. Такие сложности не остановят меня от написания своего собственного костыля.
Часть моей гениальной задумки лежала на Linq, на Trim и Last. Но Trim не работает со string, он работает c Char, то есть нужно переворачивать последний элемент листа и откусывать от строки по символу.
Linq работает не так, как я хочу – ну и не надо. Я посчитаю количество элементов в массиве и выберу нужный, а потом переверну его, разобью на char[] и обрежу таки стрингу!
Но как оказалось, не посчитаю, даже в мутабельной переменной нельзя без сильной головной боли сделать простой счетчик. Сделать то можно, но неудобно.
На этом месте я понял, что совсем ничего не понимаю и начал изучать язык.
А изучение языка я начал с просмотра чужого кода и лекций от крутых мужиков.
Это покоробило меня еще в самом начале, функция printf выводит символы в той же строке, а printfn в новой строке. В этом весь F#.
Меня, как человека знакомого с концепцией функционального программирования из Powershell это покоробило, после Powershell’a любой другой язык кажется каким-то куцым.
Если бы я делал F#, я бы сделал какую-то такую функцию:
Подход божественного павершелла к аргументам лучший, ибо не нужно держать в голове порядок указания параметров.
Сразу после своего собственного костыля я попытался сделать сайт на основе шаблона ASP NET MVC. К сожалению, из MVC там только С, но контроллеры действительно получаются очень красивые и компактные.
В F# все файлы в F# ведут себя как скрипты. Переменная или функция не объявленные выше не могут использоваться ниже.
С помощью директивы Open мы открываем неймспейсы и модули. Это аналог Using и Import-Module. По аналогии с Powershell, я могу импортнуть файл в котором есть коллекция со всеми её функциями, вставить её в середину файла и все заработает прям как в павершелле? Нет.
Если в F# файл, его мало прочитать, нужно, чтобы хотя бы одна, причем любая функция из этого файла была вызвана.
Оператор |> нужен чтобы передавать значение в функцию.
||> существует чтобы передавать кортежи в функцию.
|||> а этот монстр передает кортеж из трёх в функцию.
Работа с кортежами выглядит так:
А с единичной переменной вот так:
Тут неясно, что мешает компилятору определить с чем он имеет дело, кортежи явно указываются как кортежи и две палки со стрелочкой рядом с ними выглядят избыточно.
Эта ошибка была совершена из-за другой ошибки, <| — Pipe back оператора. Он был введен, чтобы при композиции в некоторых случаях можно было избавиться от скобочек. К примеру это:
Можно написать так:
Дон Сайм, архитектор языка как раз говорил об этом тут.
Помните о лекциях от крутых мужиков? Я прослушал лекцию от Скотта Влашина и на этом моменте меня пробило, я понял эту гигантскую мысль, осознание накрыло со всех сторон, это совсем другая парадигма. Я мог только сидеть на стуле и ухать.
Скажем, мы пытаемся прочитать файл на двух языках. В F# и С#. К примеру, пытаемся прочитать txt файл и что-то сделать с его содержимым. Если что-то пойдет не так, код написанный на C# упадет сразу в двух местах, потому что StreamReader не может прочитать файл, которого нет, да и обработчик не умеет работать с нулём.
Вся задумка состоит в том, что даже если мы не возвращаем Value, мы всегда возвращаем что-то, у нас есть тип, у нас есть Value of None. И если мы не получили Some of Value, то получили None.
Как пример, работа с дотнетовскими коллекциями в F#:
Кстати, этот же метод можно реализовать и на C# с помощью расширений, например для этого есть LanguageExt.Core и Maybe монады, но на C# все это выглядит просто ужасно.
Чтобы прочитать файл на C# мы должны писать защитный код как минимум в 2 местах. Сначала мы должны проверить, что файл существует и что файл соответствует формату, чтобы не упал streamreader.
Чтобы не падал наш процессор, нужно проверить, что файл не пустой и что он тоже правильного формата. Это абсолютно легитимный способ писать код на C#, но не на F#.
К примеру, возьмем пример, где наша программа может работать только с txt и ini файлами.
На F# защитный код пишется только в самом начале. Благодаря мощной системе типов и паттерн матчингу мы можем хендлить все варианты развития событий, не смешивая защитную логику с остальной.
Непробиваемый ни нулями, ни багами. И гениальность состоит из нескольких компонентов:
Всего 3 принципа которые даже я понял. Всего три принципа были нужны, чтобы отлавливать баги на стадии компиляции.
Это вытекает из особенности языка, все файлы в F# ведут себя как скрипты. Переменная или функция не объявленные выше не могут использоваться ниже.
Что с одной стороны, это не дает писать код в вольной спагетти манере, но с другой, становится ясно, куда смотреть. Если функция используется ниже, то она объявлена выше.
Особенно прекрасно это смотрится на бизнес-логике связанной с ASP .NET. Все типы и все функции, связанные с определенной страницей на сайте – все на одном листе.
На первый взгляд может показаться, что это просто оверхед по памяти и работа для сборщика мусора. Но как же приятно отлаживать код, когда знаешь, где и какая переменная изменилась.
Ты знаешь, что может меняться потому, что сам указал на то, что может меняться.
Так же я заметил, что чаще всего работаю с коллекциями, стрингбилдерами и т.п., зачем мне мутабельные ссылки на них?
Domain Driven Design в F# это абсолютно нативная вещь и пожалуй, лучший способ разработки. Если вы начнете писать на F#, то сможете и не заподозрить, что начали так делать.
Скажем, мы храним в базе данных данные о пользователях, где часть из них кошки, а другая – попугаи и нам нужно понять, с кем мы имеем дело. У пользователя есть поле с его ID и булёвое поле «HaveWings».
То вот это не F# и не DDD:
В этом случае мы не используем паттерн матчинг, что делает его нерасширяемым и мы не используем типы, поэтому компилятор нам больше не помощник.
А это уже и F# и DDD:
Тут мы обозначили алгебраические типы, и теперь в случае расширения кода, при добавлении еще одного типа пользователей, компилятор напомнит нам о всех местах, где нужно отхендлить новый тип.
Плюс мы используем паттерн матчинг, что в будущем, когда модель данных станет сложнее, позволит нам избавиться от вложенных else if и длинных свитчей.
В целом, можно программировать на F# и без DDD, но если можно сделать код человекопонятным, вот почему бы и нет?
Все то время, что я не знал, что пытался писать на смеси C# и Powershell даже не понимая того, что в F# то, как ты пишешь код так же важно, как и соблюдать синтаксис.
Я понял в чем суть имутабельности по дефолту, я понял DDD, я понял, в чем главная задумка языка.
Так я полюбил 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"
