[.NET, C#, F#] Букварь по F# для любопытствующих C#-разработчиков (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
07-Фев-2021 18:30

Предисловие
Мой переход на F# в качестве излюбленного языка был слегка усеян препятствиями. Примерно через десять лет почти постоянного использования C# у меня пробудилось любопытство, когда я услышал об этом другом #-языке. Моя первая реакция была той, которую с тех пор видел у других C#-разработчиков — отрицание, — C# является хорошим языком, и мне с ним комфортно, так зачем тратить силы на изучение другого? Но любопытство осталось — и, по крайней мере, несколько раз выделил вечер, чтобы прочитать базовый вводный пост и попытаться написать каких-нибудь ката на F#. Это не прижилось, потому что я просто чувствовал себя потерянным и не мог воплотить свой опыт использования C# в ощущение даже отдаленного комфорта с F#. Достаточно легко опустить фигурные скобки, немного замяться, чтобы не забыть let вместо var — но как сделать то, что я хотел?
Тогда я этого не осознавал, но, на мой взгляд, наблюдал потенциальный недостаток в том, как F#-разработчики говорят, описывают и представляют свой язык внешнему миру. Существует обширная база материалов обо всех возможностях и функциональности F#: Algebraic Data Types, Exhaustive Matching, Type Inference и т.д. Есть много статей, посвященных тому, как решать широкий спектр задач с помощью F#. Но, как мне кажется, не хватает чего-то вроде следующего: некоторых указаний о том, как взять то, что вам уже удобно в C#, и перевести их на F#. Так что мне интересно, можем ли мы как-то закрыть этот недостаток.
При этом от читателя требуется немного — поверхностное знакомство с тремя основными моментами синтаксиса F#:
  • let используется как var в C# — для объявления переменной;
  • |> — это оператор пайпа (piping) в F#, который берет результат левой части и передает его в качестве аргумента для правой части;
  • F# использует строчные буквы и апостроф для аннотаций обобщенного типа, поэтому SomeType<T> представлен как SomeType<'a>.

Остальное должно быть понятно из практики и контекста по мере продвижения. Это не должно быть исчерпывающим, замысловатым руководством, но обладать достаточной информацией, чтобы охватить большинство начальных вопросов и поставить людей на правильный путь. Букварь, если хотите.

Мне необходимо
Работать с коллекциями
В F# базовые типы коллекций (в основном) как правило очень похожи на C#, но часто имеют (иногда незначительные) различия в поведении для обеспечения иммутабельности. В большинстве случаев функции, которые работают с этими коллекциями, будут возвращать ссылки и не будут изменять содержимое исходной ссылки.
Подобрать тип коллекции
Что-то похожее на Array<T>
Тебе повезло! Массивы в F# такие же как в C#. Однако следует отметить несколько моментов:
  • Массивы в F# обычно используют нотацию [|element|], потому что [] — это нотация для списков в F#.
  • Для разделения элементов коллекции в F# используется точка с запятой, а не запятая: [|elementA;elementB|].
  • Для доступа по индексу в F# требуется префиксная точка перед фигурными скобками:
    let myArray = [|1;2;3|]
    myArray.[1] // 2

  • F# также предлагает многомерные массивы до 4-х измерений через типы Array2<'a>, Array3<'a> и Array4<'a>.

Что-то похожее на List<T>
По умолчанию в F# тип списка немного отличается от типа List<T> в C#.
Вот что вам нужно знать:
  • Списки в F# обычно используют нотацию [element] в отличие от массивов.
  • Списки, как и массивы, разделяют элементы точками с запятой вместо запятых: [elementA;elementB]
  • Списки в F# реализованы как односвязные списки — это означает, что добавление отдельных элементов выполняется в начале списка с помощью оператора :::
    let myList = [1;2;3]
    4 :: myList // [4;1;2;3]

  • Если нам необходимо добавить в конец, мы можем использовать оператор @ для объединения двух списков:
    let listA = [1;2]
    let listB = [3;4]
    listA @ listB // [1;2;3;4]


Что-то похожее на Dictionary<TKey,TValue>
По мотивам списка «выглядит похоже, но не нет» — F# предоставляет стандартный Map<'key,'value> тип, который не является родным для C# Dictionary<TKey,TValue>, но реализует обычную группу интерфейсов .NET, таких как IDictionary<TKey,TValue> и IEnumerable<T>
Вот что вам нужно знать:
  • Словари могут быть созданы из любой коллекции двух элементных кортежей, где первый элемент является ключом, а второй — значением:
    [(1,2);(3,4)] |> Map.ofList // [1] = 2, [3] = 4

  • Если создаем из последовательности, где есть дубликаты, то последний элемент для данного ключа является значением:
    [(1,2);(1,3)] |> Map.ofList |> Map.find 1 = 3 // true

  • Верен и обратный процесс: словари можно легко превратить в коллекции кортежей из двух элементов:
    [(1,2);(3,4)] |> Map.ofList |> Map.toList // [(1,2);(3,4)]

  • Встроенный тип Map в F# не очень хорошо подходит для использования в C#, в случаях интеропа мы можем создать более удобный для C# словарь IDictionary, используя функцию dict с любой коллекцией кортежей из двух элементов. Но учтите, что это по-прежнему неизменяемая структура, и при попытках добавить в нее элементы будет генерироваться исключение.
    [(1,2);(3,4)] |> dict


Подобрать функцию
Одно важное различие между F# и C#, когда дело доходит до работы с коллекциями, заключается в том, что в C# вы, как правило, оперируете над экземпляром коллекции, используя метод этого типа через точку; в то время как F# предпочитает предоставлять семейства функций в модулях, которые принимают экземпляры в качестве аргумента. Итак, C#-вариант myDictionary.Add(someKey,someValue) в F# будет Map.add someKey someValue myMap.
Просто хочу свой LINQ
F# предлагает функции, аналогичные тем, с которыми программисты на C# знакомы по LINQ, но названия часто отличаются, поскольку F# использует систему условных обозначений, которая больше соответствует терминологии, используемой в остальной части мира функционального программирования. Будьте уверены, они в основном ведут себя так, как вы и ожидаете. Дабы не утомлять — LINQ огромен, — я сопоставлю, по моему опыту, наиболее распространенные методы LINQ и их аналоги на F#:
  • .Aggregate() именуется как .fold или .reduce, в зависимости от того, предоставляете ли вы начальное состояние или просто используете первый элемент, соответственно;
  • .Select() именуется как .map;
  • .SelectMany() именуется как .collect;
  • .Where() именуется как .where или .filter (одно и то же, два имени, длинная история)
  • .All() именуется как .forall;
  • .Any() именуется как .exists, если мы подаем предикат, или .isEmpty, если мы просто хотим знать, есть ли в коллекции какие-либо элементы;
  • .Distinct() по-прежнему как .distinct или .distinctBy, если мы подаем функцию проекция;
  • .GroupBy() по-прежнему как .groupBy;
  • .Min() и .Max() по-прежнему остаются как .min и .max с альтернативами .minBy и .maxBy для использования проекции
  • .OrderBy() именуется как .sortBy, и аналогично .OrderByDescending() именуется как .sortbyDescending;
  • .Reverse() именуется как .rev;
  • .First() именуется как .head, если нам нужен первый элемент, или .find, если нам нужен первый элемент, который соответствует предикату. Точно так же вместо .FirstOrDefault() мы используем .tryHead и .tryFind, которые вернут Option, являющимся либо Some matchingValue, либо None, когда он не найден, вместо того, чтобы выбрасывать исключение;
  • .Single() именуется как .exactlyOne, и аналогично .SingleOrDefault() именуется как .tryExactlyOne.

Не уверен, какая функция нужна. У меня есть
Коллекция, а хочу
Отдельное значение или элемент
  • .min, .minBy, .max и .maxBy найдут элемент коллекции относительно других;
  • .sum, .sumBy, .average, .averageBy;
  • .find, .tryFind, .pick и .tryPick позволят найти один конкретный элемент коллекции;
  • .head, .tryHead, .last и .tryLast найдут элементы из начала или конца коллекции;
  • .fold и .reduce позволят применить логику и использовать каждый элемент коллекции для формирования другого значения;
  • .foldBack и .reduceBack делают то же самое, но с конца коллекции.

Равное количество элементов
  • .map позволит преобразовать каждый элемент коллекции;
  • .indexed свернет каждый элемент вашей коллекции в кортеж, первым элементом которого является индексом в коллекции: например, [1] станет [(0,1)];
  • .mapi делает это неявно, учитывая индекс в качестве дополнительного первого аргумента функции маппинга;
  • .sort, .sortDescending, .sortBy и .sortByDescending позволяют изменить порядок вашей коллекции.

Возможно меньшее количество элементов
  • .filter вернет коллекцию, содержащую только элементы, соответствующие указанному предикату;
  • .choose похож на .filter, но заодно позволяет маппить элементы;
  • .skip вернет оставшиеся элементы после игнорирования первых n;
  • .take и .truncate возвращают первые n-элементов, выбрасывая или нет исключение, соответственно;
  • .distinct и .independentBy позволят удалить дубликаты из коллекции.

Возможно большее количество элементов
  • .collect применит функцию создания коллекции к каждому элементу вашей коллекции и объединит все результаты воедино.

Чтобы изменить форму коллекции
  • .windowed вернет новую коллекцию всех групп размером n из исходной коллекции: например, [1; 2; 3] станет [[1; 2]; [2; 3]], когда n = 2;
  • .groupBy вернет новую коллекцию кортежей, где первый элемент является ассоциативным ключом, а второй — набором начальных элементов, которые соответствуют ассоциации: например, [1; 2; 3], преобразованной (fun i -> i % 2), приведет к [(0, [2]); (1, [1; 3])];
  • .chunkBySize вернет новую коллекцию, содержащую до n коллекций оригинала: например, [1; 2; 3] станет [[1; 2]; [3]], когда n = 2;
  • .splitInto вернет новую коллекцию, содержащую n коллекций одинакового размера из исходного: например, [1; 2; 3] станет [[1]; [2]; [3]], когда n = 3.

Чтобы пройти по коллекции без ее изменения
  • .iter и .iteri берут и применяют функцию к каждому элементу вашей коллекции, но не возвращают никакого значения.

Отдельное значение и хочу
Чтобы было частью коллекции
  • .singleton можно использовать для создания коллекции из одного элемента из значения;
  • .init примет размер и функцию инициализатора и создаст новую коллекцию этого размера.

Несколько коллекций и хотите
Скомбинировать их
  • .append принимает две коллекции и создает новую единую коллекцию, содержащую все элементы обеих;
  • .concat делает то же самое, но для коллекции коллекций;
  • .map2 и .fold2 действуют как выше указанные .map и .fold, но будут предоставлять элементы из одного индекса в двух исходных коллекциях для функции маппинга / свертки;
  • .allPairs принимает две коллекции и образует все перестановки по 2 элемента между ними;
  • .zip и .zip3 берут 2 (или 3) коллекции и создают одну коллекцию, состоящую из кортежей элементов из одного индекса в источниках.

Работать асинхронно
Модель асинхронности в F# похожа на модель в C#, но имеет несколько важных отличий, которые иногда застают врасплох C#-разработчиков:
  • F# имеет отдельный тип Async<'t>, похожий на Task<T> в C#.
  • Из-за того, что система типов F# требует возврата, она использует Async<unit> вместо Task в случаях, когда мы не возвращаем фактического значения.
  • F# может генерировать и использовать Task<T> с помощью функций Async.StartAsTask и Async.AwaitTask из базовой библиотеки.

У F# есть еще одно очень заметное отличие от C# в отношении асинхронного кода: C# "включает" ключевое слово await внутри метода, применяя ключевое слово async к сигнатуре этого метода; F# использует языковую функцию, называемую computation expression, в результате чего асинхронность становится частью тела функции. Это также имеет некоторые последствия на то, как вы пишете код внутри этого тела функции:
let timesTwo i = i * 2 // У нас есть определение нашей базовой функции
// И теперь мы можем сделать это асинхронным
let timesTwoAsync i = async { // Обратите внимание, что при работе с computation expression мы начинаем с нашего ключевого слова, а затем с самой функции внутри фигурных скобок
   return i * 2 // Мы также используем ключевое слово `return` для завершения выражения
}
let timesFour i = async {
    let! doubleOnce = timesTwoAsync i // Обратите внимание  на `!` в нашем `let!` — это похоже на `await` в C# — правосторонняя функция должна возвращать `Async<'a>`
    // После того, как мы связали результат асинхронной функции с помощью `let!` — мы можем использовать его потом как обычно
    let doubleTwice = timesTwo doubleOnce // В случае неасинхронных функций мы можем написать наш код как обычно
    return doubleTwice
}

  • Имейте в виду, что let! в Async-блоках работают только при вызове Async-образующих функций — аналогично тому, как в C# await можно использовать только для методов, возвращающих Task.
  • Другой путь, однако, заключается в том, что поскольку F# обрабатывает асинхронность исключительно в теле функций, нет никаких требований о том, какие функции вы можете связывать с let! — все, что возвращает Async<'a>, допустимо. Это противоположно требованиям C# о том, что вы можете применять await только к методам, помеченным как async.

Сообщать об ошибке или контролировать выполнение программы
Во-первых, определение: когда мы говорим об ошибках и выполнении программы, я не имею в виду исключения — в F# они есть и вполне схожим образом работают как в C#. Я имею в виду предсказуемые и потенциально исправимые ошибки; потому что эта та область, в которой F# с первого взгляда может показаться похож на C#, но очень быстро становится очевидно, насколько они различаются. В частности, это проявляется в использовании значения null как распространенного сигнала об ошибки в C#. Это не редкий паттерн в C#, который выглядит примерно так:
public Foo DoSomething(Bar bar)
{
    if (bar.IsInvalid)
    {
        return null;
    }
    return new Foo(bar.Value);
}

И затем, вызывающий DoSomething может проверить возвращаемое значение на null и либо обработать, либо передать его дальше. По моему опыту, одна из областей, где это часто возникает — это функция LINQ FirstOrDefault(), которая используется, чтобы избежать исключения в случае пустого IEnumerable<T>, но часто заканчивается просто продвижением дальше null.
Изначально кажется, что F# пытается осуществить это с помощью своего типа Option<'a> — и часто возникает вопрос: не является ли None просто ярлыком для null, за исключением того, что теперь труднее получить значение обернутое в Some? Потому что для этого потребуется pattern matching или проверка .HasValue для опции — и действительно ли это лучше? Это не так, и именно поэтому F# посредством функционального программирования предлагает более чистое решение: разрабатывать основную часть кодовой базы, не беспокоясь о проверке на существующие ошибки, а вместо этого беспокоясь только об оповещении потенциально новых, специфичных для данной функции. Мы можем сделать это, написав большинство наших функций так, как будто входные данные уже были проверены для нас, и затем, с помощью функций map или bind, связать наши безответственные функции вместе. Давайте посмотрим на них в контексте Option:
  • map требуется два аргумента: функция 'a -> 'b и Option<'a>, из которых она будет генерировать Option<'b>;
  • bind также требует два аргумента: функция 'a -> Option<'b> и Option<'a>, из которых она будет генерировать Option<'a>.

Давайте посмотрим, что они могут для нас сделать:
// string -> Option<string>
let getConfigVariable varName =
    Private.configFile
    |> Map.tryFind varName
// string -> Option<string[]>
let readFile filename =
    if File.Exists(filename)
        then Some File.ReadLines(filename)
        else None
// string[] -> int
let countLines textRows = Seq.length file
getConfigVariable "storageFile"                 // 1
|> Option.bind readFile                         // 2
|> Option.map countLines                        // 3

Так что тут происходит?
  • Мы пытаемся взять переменную из нашей конфигурации. Может быть, она существует, а может и нет, но это имеет значение только для этой единственной функции.
  • Затем мы перенаправляем в Option.bind — который неявно обрабатывает логику безопасности для нас: если предыдущий шаг имеет значение Some — используйте его в качестве аргумента этой функции, — в противном случае оставьте его как None и двигайтесь дальше.
  • Option.map делает то же самое — если есть значение Some, используйте его с этой функцией, в противном случае просто двигайтесь дальше.

Прозорливый наблюдатель заметит, что на шаге 3 нет непосредственной разницы между bind и map — они оба автоматически обрабатывают одно и то же, верно? Но обратите внимание на разные сигнатуры между readFile и countLinesbind имеет дополнительный шаг, который производит flatten (прим. перев.: разворачивает вложенную структуру, Option.flatten) над параметром Option, который выводит его функция. Рассмотрим альтернативу: если бы мы использовали map, то в конце строки 2 у нас было бы Option<Option<string[]>> — и так в строке 3 нам потребуется Option.map (Option. map countLines)!
Но возникает вопрос, как мне на самом деле получить значение, если оно есть выводом этого Option? И это справедливый вопрос. И ответ — избегать этого как можно дольше. Поскольку, чем позже вы откладываете попытку развернуть Option, тем меньше кода вам нужно написать, который хоть как-то предполагает, что ошибка возможна. И в тот момент, когда вам, наконец, определенно необходимо получить значение, у вас есть два варианта:
  • Option.defaultValue принимает 'a и Option<'a> — если Option имеет значение, он возвращает его, в противном случае он возвращает значение 'a, которое вы ему дали.
  • Option.defaultWith — то же самое, но вместо значения для генерации значения требуется функция unit -> 'a.

Так уж совпало, что та же самая логика применима к встроенному в F# типу Result<'a,'b>, который также предлагает bind и mapmapError, если вам это нужно) — но вместо None у вас есть вариант Error, который вы можете использовать для хранения информации о том, что пошло не так — будь то string или пользовательский тип ошибки по вашему выбору.
Использовать C#-библиотеки в F
Одно из восхитительных преимуществ F# — и, вероятно, почему C#-разработчик сначала смотрит на него, а не на что-то вроде Haskell, — это то, что он является частью большой экосистемы .NET и поддерживает взаимодействие со всеми C#-библиотеками, с которыми разработчик уже знаком. Код на C# может (в основном) использоваться в F#, но иногда возникают некоторые затруднения, но обычно с легкими обходными путями:
  • При вызове C#-методов компилятор F# рассматривает метод как кортеж с одним аргументом. Из-за этого частичное применение строго невозможно, и пайпинг может быть затруднен из-за перегрузки:
    "1" |> Int32.Parse                          // Подобно Int32.Parse("1")
    ("1", NumberStyles.Integer) |> Int32.Parse  // Подобно Int32.Parse("1", NumberStyles.Integer)
    NumberStyles.Integer |> Int32.Parse "1"     // Не компилируется, потому что ожидает кортежный аргумент, а не два отдельных аргумента.

  • C#-Библиотеки — особенно те, которые включают сериализацию или рефлексию, — часто не приспособлены для понимания встроенных типов F#. Наиболее распространенным случаем здесь являются библиотеки JSON, которые могут затрудняются над сериализацией и/или десериализацией Unions и Records — в таких случаях настоятельно рекомендуется проверить на существование библиотеки расширений, которая предоставляет специфичную функциональность F#. Например, Newtonsoft.Json имеет пакет Newtonsoft.Json.FSharp, System.Text.JsonFSharp.SystemTextJson. С другой стороны, в этих случаях может быть также хорошо проверить нативные библиотеки на F# подобно Thoth или Chiron.
  • Благодаря возможности C# создавать null для любого ссылочного типа, и отсутствию (на момент написания) (прим. перев.: fsharp/fslang-suggestions#577) встроенного интеропа для обозначения nullable reference type в C#, полезно попытаться изолировать код C# на внешнем уровне вашей логики и использовать утилиты, такие как Option.ofNullable (для Nullable<T>) или Option.ofObj (для ссылочных типов), чтобы быстро обеспечить безопасность типов для вашего собственного кода.
  • Методы в C#, которые ожидают типы делегатов, такие как Action<T> или Func<T>, могут получить лямбда-выражение F# соответствующей сигнатуры, и компилятор будет обрабатывать преобразование. Помните: unit заменяет void в F# — и его () значение — поэтому Action<T> будет ожидать 'T -> unit, например (fun _ -> printfn "I'm a lambda!"); и аналогично, Fun <T> ожидает unit -> 'T, например (fun () -> 123).
  • В тех случаях, когда C#-библиотека ожидает, что объекты будут декорированы атрибутами, то для этого используется хитрость в виде <>, которую F# использует внутри квадратных скобок — так что [Serializable] C# превратится в [<Serializable>] F#. Аргументы работают одинаково: [<DllImport('user32.dll', CharSet = CharSet.Auto)>]. И, как и в случае с коллекциями выше, несколько атрибутов разделяются точкой с запятой, а не запятой: например, [<AttributeOne; AttributeTwo>].

===========
Источник:
habr.com
===========

===========
Автор оригинала: Ryan Coy
===========
Похожие новости: Теги для поиска: #_.net, #_c#, #_f#, #_.net_c#_f#_primer, #_.net, #_c#, #_f#
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 12:20
Часовой пояс: UTC + 5