company_banner

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


    RUVDS.com
    VDS/VPS-хостинг. Скидка 10% по коду HABR

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

      +1

      Касательно printfn: недавно добавили интерполяцию строк как в С#:
      https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/interpolated-strings

      +3

      В кусках кода


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

      и


      это уже и F# и DDD

      один и тот же код :)

        +3

        Код не меняется, меняется только твое восприятие. Но это я так, шучу)

          0
          я даже в Фотошоп полез это проверять, наложением скриншотов )))
            0

            UPD: Второй кусок кода поправили, но там теперь отсутствует упоминание HaveWings ¯\_(ツ)_/¯

              0

              Стало похоже на Elixir на минималках, имхо.

                +1
                Код зависит от последовательности определения полей в таблице БД? Чёт как-то жёстко, но возможно я просто не понимаю это всё.
              0

              После F# остались только негативные эмоции. Размытый неконсистентный синтаксис, неуклюжий компилятор, миллион операторов, которые никто не подсказывает, язык постоянно лезет не в свое дело. Одно отсутствие циклического импорта чего стоит.
              После Swift язык ощущается как ужасный самопал. В Swift для тех же Option придумали if case, if let, guard let, switch, optional chaining, optional init, implicitly unwrapped, все чтобы помочь программисту. В F#, насколько я понял, это только switch, который раздувает код. При этом в Swift value type это правда value type, а тут кто-то вернул из .NET record и пробуй обработать null, тебе пишут, что record не может быть null, ну да ну да. В Swift такие вещи маппятся в optional и никаких вопросов.
              Или те же коллекции, которые не ведут себя как в .NET. Ну сделайте свой пакет коллекций, в чем проблема-то? Везде одни проблемы с этим языком.

                0
                это только switch, который раздувает код

                Есть же ещё конструкции наподобие TaskBuilder и MaybeBuilder, которые позволяют делать optional и task chaining, но через переменные — как раз чтобы не обкладывать всё конструкциями match:


                let onHello context =
                  maybe {
                    let! message = context.Update.Message
                    let! name = message.Chat.FirstName
                    sprintf "Hello, %s!" name
                    |> sendMessage message.Chat.Id
                  } |> ignore

                Здесь, context.Update.Message — это Option<Message>. Аналогично с FirstName. В случае maybe после let! произойдён early return, если в option лежит None.

                  0
                  "...Swift для тех же Option придумали… В F#, насколько я понял, это только switch, который раздувает код..."
                  Если какая-то switch-логика уже упакована в функции (особенно стандартные), то switch не нужен:
                  let t1 = Some("test")
                  t1 |> Option.map(printfn "%A is mapped")
                  t1 |> Option.filter(fun t -> t.Length>5) |> Option.map(printfn "%A is mapped again")

                  +6
                  Хм, не такой уж умный компилятор???? Или все же автор статьи не понял как задаются типы, в примере `let splitPath inputObject: string ` string задает тип функции, а не параметра. А надо вот так: `let splitPath (inputObject: string) = `…
                    +2
                    Тут неясно, что мешает компилятору определить с чем он имеет дело, кортежи явно указываются как кортежи и две палки со стрелочкой рядом с ними выглядят избыточно.

                    А если функция принимает кортеж? А если функция полиморфна? Невозможно это определять компилятором в общем случае.


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

                    А как он это поймет, метод Split может быть у чего угодно, тип никак не указан, а каких-нибудь анонимных интерфейсов насколько я знаю в языке нет (чтобы у аргумента был тип "любой тип, который содержит метод Split")
                    достаточно было просто указать тип аргументу


                    Абсолютно не понятно, зачем писать так:


                    let splitPath inputObject = 
                      let mutable inputObject : string = inputObject
                      ...

                    когда можно просто так:


                    let splitPath (inputObject: string) = 
                      ...
                      0
                      А как он это поймет, метод Split может быть у чего угодно, тип никак не указан, а каких-нибудь анонимных интерфейсов насколько я знаю в языке нет (чтобы у аргумента был тип «любой тип, который содержит метод Split»)
                      Можно поизвращаться и написать функцию, принимающую что угодно, имеющее метод Split, но кроме строк его ни у кого нет, так что это лишено смысла.
                      let inline splitPath path =
                          (^t: (member Split: char array -> string array) (path, [|'\\'; '/'|]))
                      +3

                      По поводу кода, f# не знаю, но на хаскеле было бы так:


                      splitPath :: Text -> Text
                      splitPath = Text.intercalate "/" . init . Text.splitOn "/"

                      подозреваю что на f# примерно так же, смог написать только так:


                      let init xs = Seq.take (Seq.length xs - 1) xs
                      
                      let splitPath (path: string) = 
                        path.Split "/" |> init |> String.concat "/"
                      
                      printf "%s" (splitPath "C:/users/test/folder")
                        0

                        про проверки на null


                        В c# можно выставить nullable enable и использовать ?/! и атрибуты

                          +1
                          Автор, я, конечно, всё понимаю, но ваш первый пример на F# выглядит вот так:
                          @"C:\Users\test\folder".Split '\\' |> (Seq.rev >> Seq.skip 1 >> Seq.rev >> String.concat "\\")
                          Или вот так, если использовать индексирование:
                          let s = @"C:\Users\test\folder" in s.[.. s |> Seq.findIndexBack ((=) '\\')]
                          Не надо пытаться писать на ML-подобном языке в императивном стиле, это в принципе возможно, но получается очень страшно.
                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              Не такой уж и умный компилятор

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

                              Но вы и не указали тип входного параметра. Вы указали возвращаемый тип — string.
                              Тип входного параметра указывается так:


                              let splitPath (inputObject : string) : string =

                                +1
                                Вот так этот велосипед выглядит на Powershell:

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

                                Во-первых, велосипед можно сделать по-красивее

                                $Path = 'C:\users\test\folder'
                                $Path.Trim($Path.Split('\')[-1])
                                

                                Во-вторых, даже тестовая задача должна иметь какой-то смысл.
                                Удалить из полного пути папки символы 'd', 'e', 'f', 'l', 'o', 'r' — не представляю, где это может пригодиться.
                                Напомню, метод Trim(Char[]) удаляет все начальные и конечные вхождения набора символов, указанного в массиве, из текущей строки.
                                В-третьих, в F# это выглядит так же красиво без всяких мутабельных переменных
                                let path = @"C:\users\test\folder"
                                let myTrim (str: string) = 
                                    str.Trim((str.Split('\\') |> Array.last).ToCharArray())
                                myTrim path

                                А System.IO.Path.DirectorySeparatorChar можно и в Powershell вставить
                                $Path.Trim($Path.Split([System.IO.Path]::DirectorySeparatorChar)[-1])

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

                                Самое читаемое