company_banner

Погружение в F#. Пособие для C#-разработчиков

https://blogs.msdn.microsoft.com/dotnet/2017/07/24/get-started-with-f-as-a-c-developer/
  • Перевод

Этот пост будет не о том, как «перевести» код с C# на F#: различные парадигмы делают каждый из этих языков лучшим для своего круга задач. Однако вы сможете оценить все достоинства функционального программирования быстрее, если не будете думать о переводе кода из одной парадигмы в другую. Настало время любопытных, пытливых и готовых изучать совершенно новые вещи. Давайте начнем!



Ранее, в посте «Почему вам следует использовать F#», мы рассказали, почему F# стоит попробовать прямо сейчас. Теперь мы разберем основы, необходимые для его успешного применения. Пост предназначен для людей, знакомых с C#, Java или другими объектно-ориентированными языками. Если вы уже пишете на F#, эти понятия должны быть вам хорошо знакомы.


Сразу к различиям


Перед тем, как приступить к изучению понятий функционального программирования, давайте посмотрим на небольшой пример и определим, в чем F# отличается от C#. Это базовый пример с двумя функциями и выводом результата на экран:


let square x = x * x

let sumOfSquares n =
    [1..n] // Создадим список с элементами от 1 до n
    |> List.map square // Возведем в квадрат каждый элемент
    |> List.sum // Просуммируем их!

printfn "Сумма квадратов первых 5 натуральных чисел равна %d" (sumOfSquares 5)

Обратите внимание, что здесь нет явного указания типов, отсутствуют точки с запятой или фигурные скобки. Скобки используются в единственном месте: для вызова функции sumOfSquares с числом 5 в качестве входного значения и последующего вывода результата на экран. Конвейерный оператор |> (pipeline operator) используется так же, как конвейеры (каналы, pipes) в Unix. square — это функция, которая напрямую передается в функцию List.map как параметр (функции в F# рассматриваются как значения, first-class functions).


Хотя различий на самом деле еще много, сперва стоит разобраться с фундаментальными вещами, поскольку они — ключ к пониманию F#.


C# и F#: Соответствие ключевых понятий


Следующая таблица показывает соответствия между некоторыми ключевыми понятиями C# и F#. Это умышленно короткое и неполное описание, но так его проще запомнить в начале изучения F#.


C# и Объектно-Ориентированное Программирование F# и Функциональное Программирование
Переменные Неизменяемые значения
Инструкции Выражения
Объекты с методами Типы и функции

Быстрая шпаргалка по некоторым терминам:


  • Переменные — это значения, которые могут меняться. Это следует из их названия!


  • Неизменяемые значения — это значения, которые не могут быть изменены после присваивания.


  • Инструкции — это команды, исполняемые после запуска программы.


  • Выражения — это фрагменты кода, которые можно вычислить и получить значения.


  • Типы — это классификация данных в программе.

Стоит отметить, что все указанное в столбце C# так же возможно в F# (и довольно легко реализуется). В столбце F# также есть вещи, которые можно сделать в C#, хотя и намного сложнее. Следует упомянуть, что элементы в левом столбце не являются "плохими" в F#, и наоборот. Объекты с методами отлично подходят для использования в F# и часто являются лучшим решением в зависимости от вашей ситуации.


Неизменяемые значения вместо переменных


Одним из наиболее непривычных понятий в функциональном программировании является неизменяемость (иммутабельность, immutability). Ему часто уделяют недостаточно внимания в сообществе любителей функционального программирования. Но если вы никогда не использовали язык, в котором значения неизменяемы по умолчанию, это часто является первым и наиболее значимым препятствием для дальнейшего изучения. Неизменяемость является фундаментальным понятием практически во всех функциональных языках.


let x = 1

В предыдущем выражении значение 1 связано с именем x. В течение всего времени существования имя x теперь ссылается на значение 1 и не может быть изменено. Например, следующий код не может переназначить значение x:


let x = 1
x = x + 1 // Это выражение ничего не присваивает!

Вместо этого, вторая строка является сравнением, определяющим, является ли x равным x + 1. Хотя существует способ изменить (мутировать, mutate) x с помощью использования оператора <- и модификатора mutable (см. подробности в Mutable Variables), вы быстро поймете, что проще думать о решении задач без переприсвоения значений. Если не рассматривать F# как еще один императивный язык программирования, вы сможете использовать его самые сильные стороны.


Неизменяемость существенным образом преобразует ваши привычные подходы к решению задач. Например, циклы for и другие базовые операции императивного программирования не так часто используются в F#.


Рассмотрим более конкретный пример: вы хотите возвести в квадрат числа из входного списка. Вот как это можно сделать в F#:


// Определим функцию, которая вычисляет квадрат значения
let square x = x * x

let getSquares items =
    items |> List.map square

let lst = [ 1; 2; 3; 4; 5 ] // Создать список в F#

printfn "Квадрат числа %A равен %A" lst (getSquares lst)

Заметим, что в этом примере нет цикла for. На концептуальном уровне это сильно отличается от императивного кода. Мы не возводим в квадрат каждый элемент списка. Мы применяем функцию square к входному списку и получаем значения, возведенные в квадрат. Это очень тонкое различие, но на практике оно может приводить к значительно отличающемуся коду. Прежде всего, функция getSquares на самом деле создает полностью новый список.


Неизменяемость — это гораздо более широкая концепция, чем просто иной способ управления данными в списках. Понятие ссылочной прозрачности (Referential Transparency) естественно для F#, и оказывает значительное влияние, как на разработку систем, так и на то, как части этих систем сочетаются. Функциональные характеристики системы становятся более предсказуемыми, когда значения не изменяются, если вы этого не ожидаете.


Более того, когда значения неизменяемы, конкурентное программирование становится проще. Некоторые сложные проблемы, возникающие в С# из-за изменяемого состояния, в F# не встречаются вообще. F# не может волшебным образом решить все ваши проблемы с многопоточностью и асинхронностью, однако он сделает многие вещи проще.


Выражения вместо инструкций


Как было упомянуто ранее, F# использует выражения (expressions). Это контрастирует с C#, где практически для всего используются инструкции (statements). Различие между ними может казаться на первый взгляд незначительным, однако есть одна вещь, о которой следует помнить: выражения производят значения. Инструкции — нет.


// 'getMessage' -- это функция, и `name` - ее входной параметр.
let getMessage name =
    if name = "Phillip" then   // 'if' - это выражение.
        "Hello, Phillip!"      // Эта строка тоже является выражением. Оно возвращает значение
    else
        "Hello, other person!" // То же самое с этой строкой.

let phillipMessage = getMessage "Phillip" // getMessage, при вызове, является выражением. Его значение связано с именем 'phillipMessage'.
let alfMessage = getMessage "Alf" // Это выражение связано с именем 'alfMessage'!

В предыдущем примере вы можете увидеть несколько моментов, которые отличают F# от императивных языков вроде C#:


  • if...then...else — это выражение, а не инструкция.
  • Каждая ветка выражения if возвращает значение, которое в данном случае будет являться возвращаемым значением функции getMessage.
  • Каждый вызов функции getMessage — это выражение, которое принимает строку и возвращает строку.

Этот подход сильно отличается от C#, но скорее всего он покажется вам естественным при написании кода на F#.


Если копнуть немного глубже, в F# даже инструкции описываются с помощью выражений. Такие выражения возвращают значение типа unit. unit немного похож на void в C#:


let names = [ "Alf"; "Vasily"; "Shreyans"; "Jin Sun"; "Moulaye" ]

// Цикл `for`.  Ключевое слово 'do' указывает, что выражение их внутренней области видимости должно иметь тип `unit`.
// Если это не так, то результат выражения неявно игнорируется.
for name in names do
    printfn "My name is %s" name // printfn возвращает unit.

В предыдущем примере с циклом for всё имеет тип unit. Выражения типа unit — это выражения, которые не имеют возвращаемого значения.


F#: Массивы, списки и последовательности


Предыдущие примеры кода использовали массивы и списки F#. В данном разделе разъясняются некоторые подробности.


F# предоставляет несколько типов коллекций и самые распространенные из них — это массивы, списки и последовательности.



Массивы, списки и последовательности в F# также имеют особый синтаксис для выражений. Это очень удобно для различных задач, когда нужно генерировать данные программно.


// Создадим список квадратов первых 100 натуральных чисел
let first100Squares = [ for x in 1..100 -> x * x ]

// То же самое, но массив!
let first100SquaresArray = [| for x in 1..100 -> x * x |]

// Функция, которая генерирует бесконечную последовательность нечетных чисел
//
// Вызывать вместе с Seq.take!
let odds = 
    let rec loop x = // Использует рекурсивную локальную функцию
        seq { yield x
              yield! loop (x + 2) }
    loop 1

printfn "Первые 3 нечетных числа: %A" (Seq.take 3 odds)
// Вывод:  "Первые 3 нечетных числа: seq [1; 3; 5]

Соответствие между функциями F# и методами LINQ


Если вы знакомы с методами LINQ, следующая таблица поможет вам понять аналогичные функции в F#.


LINQ F# функция
Where filter
Select map
GroupBy groupBy
SelectMany collect
Aggregate fold или reduce
Sum sum

Вы также можете заметить, что такой же набор функций существует для модулей Seq, List и Array. Функции модуля Seq могут быть использованы для последовательностей, списков или массивов. Функции для массивов и списков могут быть использованы только для массивов и списков в F# соответственно. Также последовательности в F# ленивые, а списки и массивы — энергичные. Использование функций модуля Seq на списках или массивах влечет за собой ленивое вычисление, а тип возвращаемого значения будет последовательностью.


Предыдущий раздел содержит в себе довольно много информации, но по мере написания программ на F# она станет интуитивно понятной.


Функциональные конвейеры


Вы могли заметить, что оператор |> используется в предыдущих примерах кода. Он очень похож на конвейеры в unix: принимает что-то слева от себя и передает на вход чему-то справа. Этот оператор (называется «pipe» или «pipeline») используется для создания функциональных конвейеров. Вот пример:


let square x = x * x
let isOdd x = x % 2 <> 0

let getOddSquares items =
    items
    |> Seq.filter isOdd
    |> Seq.map square

В данном примере сначала items передается на вход функции Seq.filter. Затем возвращаемое значение Seq.filter (последовательность) передается на вход функции Seq.map. Результат выполнения Seq.map является выходным значением функции getOddSquares.


Конвейерный оператор очень удобно использовать, поэтому редко кто обходится без него. Возможно, это одна из самых любимых возможностей F#!


F#: типы


Поскольку F# — язык платформы .NET, в нем существуют те же примитивные типы, что и C#: string, int и так далее. Он использует объекты .NET и поддерживает четыре основных столпа объектно-ориентированного программирования. F# предоставляет кортежи (tuples), а также два основных типа, которые отсутствуют в C#: записи (records) и размеченные объединения (discriminated unions).


Запись — это группа упорядоченных именованных значений, которая автоматически реализует операцию сравнения — в самом буквальном смысле. Не нужно задумываться о том, как происходит сравнение: через равенство ссылок или с помощью пользовательского определения равенства между двумя объектами. Записи — это значения, а значения можно сравнивать. Они являются типами-произведениями, если говорить на языке теории категорий. У них есть множество применений, однако одно из самых очевидных — их можно использовать в качестве POCO или POJO.


open System

// Вот так вы можете определить тип-запись.
// Можно располагать метки на новых строках
type Person =
    { Name: string
      Age: int
      Birth: DateTime }

// Создать новую запись `Person` можно примерно так.
// Если метки расположены на одной строке, они разделяются точкой с запятой
let p1 = { Name="Charles"; Age=27; Birth=DateTime(1990, 1, 1) }

// Или же можно располагать метки на новых строках
let p2 =
    { Name="Moulaye"
      Age=22
      Birth=DateTime(1995, 1, 1) }

// Записи можно сравнивать на равенство. Не нужно определять метод Equals() и GetHasCode().
printfn "Они равны? %b" (p1 = p2) // Это выведет `false`, потому что они не равны.

Другой основной тип в F# — это размеченные объединения, или РО, или DU в англоязычной литературе. РО — это типы, представляющие некоторое количество именованных вариантов. На языке теории категорий это называется типом-суммой. Они также могут быть определены рекурсивно, что значительно упрощает описание иерархических данных.


// Определим обобщенное бинарное дерево поиска.
//
// Заметим, что обобщенный тип-параметр имеет ' в начале.
type BST<'T> =
    | Empty
    | Node of 'T * BST<'T> * BST<'T> // Каждый узел имеет левый и правый BST<'T>

// Развернем BST с помощью сопоставления с образцом!
let rec flip bst =
    match bst with
    | Empty -> bst
    | Node(item, left, right) -> Node(item, flip right, flip left)

// Определим пример BST
let tree = 
    Node(10, 
        Node(3, 
            Empty, 
            Node(6, 
                Empty, 
                Empty)),
        Node(55,
            Node(16, 
                Empty,
                Empty),
            Empty))

// Развернем его!
printfn "%A" (flip tree)

Тадам! Вооружившись мощью размеченных объединений и F#, вы можете пройти любое собеседование, в котором требуется развернуть бинарное дерево поиска.


Наверняка вы увидели странный синтаксис в определении варианта Node. Это на самом деле сигнатура кортежа. Это означает, что определенное нами BST может быть или пустым, или являться кортежем (значение, левое поддерево, правое поддерево). Более подробно про это написано в разделе о сигнатурах.


Собираем всё вместе: синтаксис F# за 60 секунд


Следующий пример кода представлен с разрешения Скотта Влашина, героя сообщества F#, написавшего этот прекрасный обзор F# синтаксиса. Вы прочтете его примерно за минуту. Пример был немного отредактирован.


// Данный код представлен с разрешения автора, Скотта Влашина. Он был немного модифицирован.
// Для однострочных комментариев используется двойной слеш. 
(* 
    Многострочные комментарии можно сделать вот так (хотя обычно используют двойной слеш).
*)

// ======== "Переменные" (на самом деле нет) ==========
// Ключевое слово "let" определяет неизменяемое (иммутабельное) значение
let myInt = 5
let myFloat = 3.14
let myString = "привет"   // обратите внимание - указывать тип не нужно

// ======== Списки ============
let twoToFive = [ 2; 3; 4; 5 ]        // Списки создаются с помощью квадратных скобок,
                                      // для разделения значений используются точки с запятой.
let oneToFive = 1 :: twoToFive        // оператор :: создает список с новым первым элементом
// Результат: [1; 2; 3; 4; 5]

let zeroToFive = [0;1] @ twoToFive    // оператор @ объединяет два списка

// ВАЖНО: запятые никогда не используются для разделения значений, только точки с запятой!

// ======== Функции ========
// Ключевое слово "let" также определяет именованную функцию.
let square x = x * x          // Обратите внимание - скобки не используются.
square 3                      // А сейчас вызовем функцию. Снова никаких скобок.

let add x y = x + y           // не используйте add (x,y)! Это означает
                              // совершенно другую вещь.
add 2 3                       // Вызовем фукнкцию.

// чтобы определить многострочную функцию, просто используйте отступы.
// Точки с запятой не требуются.
let evens list =
   let isEven x = x % 2 = 0     // Определет "isEven" как внутреннюю ("вложенную") функцию
   List.filter isEven list      // List.filter - это библиотечная функция
                                // с двумя параметрами: предикат
                                // и список, которые требуется отфильтровать

evens oneToFive                 // Вызовем функцию

// Вы можете использовать скобки, чтобы уточнить приоритет.
// В данном примере, сначала используем "map" с двумя аргументами,
// а потом вызываем "sum" для результата.
// Без скобок "List.map" была бы передана как аргумент в "List.sum"
let sumOfSquaresTo100 =
   List.sum (List.map square [ 1 .. 100 ])

// Вы можете передать результат одной функции в следующую с помощью "|>"
// Вот та же самая функция sumOfSquares, переписанная с помощью конвейера
let sumOfSquaresTo100piped =
   [ 1 .. 100 ] |> List.map square |> List.sum  // "square" определена раньше

// вы можете определять лямбда-функции (анонимные функции) 
// с помощью ключевого слова "fun"
let sumOfSquaresTo100withFun =
   [ 1 .. 100 ] |> List.map (fun x -> x * x) |> List.sum

// В F# значения возвращаются неявно - ключевое слово "return" не используется
// Функция всегда возвращает значение последнего выражения в ее теле

// ======== Сопоставление с образцом ========
// Match..with.. - это case/switch инструкции "на стероидах".
let x = "a"
match x with
| "a" -> printfn "x - это a"
| "b" -> printfn "x - это b"
| _ -> printfn "x - это что-то другое"   // подчеркивание соответствует "чему угодно"

// Some(..) и None приблизительно соответствуют оберткам Nullable<T>
let validValue = Some(99)
let invalidValue = None

// В данном примере match..with сравнивает с "Some" и "None"
// и в то же время распаковывает значение в "Some".
let optionPatternMatch input =
   match input with
    | Some i -> printfn "целое число %d" i
    | None -> printfn "входное значение отсутствует"

optionPatternMatch validValue
optionPatternMatch invalidValue

// ========= Сложные типы данных =========

// Кортежи - это пары, тройки значений и так далее.
// Кортежи используют запятые.
let twoTuple = (1, 2)
let threeTuple = ("a", 2, true)

// Записи имеют именованные поля. Точки с запятой являются разделителями.
type Person = { First: string; Last: string }

let person1 = { First="John"; Last="Doe" }
// Вы можете также использовать переносы на новую строку
// вместо точек с запятой.
let person2 =
    { First="Jane"
      Last="Doe" }

// Объединения представляют варианты. Разделитель - вертикальная черта.
type Temp = 
    | DegreesC of float
    | DegreesF of float

let temp = DegreesF 98.6

// Типы можно комбинировать рекурсивно различными путями.
// Например, вот тип-объединение, который содержит список
// элементов того же типа:
type Employee = 
  | Worker of Person
  | Manager of Employee list

let jdoe = { First="John"; Last="Doe" }
let worker = Worker jdoe

// ========= Вывод на экран =========
// Функции printf/printfn схожи с функциями Console.Write/WriteLine из C#.
printfn "Вывод на экран значений типа int %i, float %f, bool %b" 1 2.0 true
printfn "Строка %s, и что-то обобщенное %A" "hello" [ 1; 2; 3; 4 ]

// все сложные типы имеют встроенный красивый вывод
printfn "twoTuple=%A,\nPerson=%A,\nTemp=%A,\nEmployee=%A" 
    twoTuple person1 temp worker

В дополнение, в нашей официальной документации для .NET и поддерживаемых языков есть материал «Тур по F#».


Что делать дальше


Всё описанное в данном посте — лишь поверхностные возможности F#. Мы надеемся, что после прочтения этой статьи вы сможете погрузиться в F# и функциональное программирование. Вот несколько примеров того, что можно написать в качестве упражнения для дальнейшего изучения F#:



Есть очень много других задач, для которых можно использовать F#; предыдущий список ни в коем случае не является исчерпывающим. F# используется в различных приложениях: от простых скриптов для сборки до бэкенда интернет-магазинов с миллиардной выручкой. Нет никаких ограничений по проектам, для которых вы можете использовать F#.


Дополнительные ресурсы


Для F# существует множество самоучителей, включая материалы для тех, кто пришел с опытом C# или Java. Следующие ссылки могут быть полезными по мере того, как вы будете глубже изучать F#:



Также описаны еще несколько способов, как начать изучение F#.


И наконец, сообщество F# очень дружелюбно к начинающим. Есть очень активный чат в Slack, поддерживаемый F# Software Foundation, с комнатами для начинающих, к которым вы можете свободно присоединиться. Рекомендуем вам это сделать!


Не забудьте посетить сайт русскоязычного сообщества F#! Если у вас возникнут вопросы по изучению языка, мы будем рады обсудить их в чатах:



Об авторах перевода


Статья переведена усилиями русскоязычного сообщества F#-разработчиков.
Мы также благодарим @schvepsss за подготовку данной статьи к публикации.

Метки:
Microsoft 401,78
Microsoft — мировой лидер в области ПО и ИТ-услуг
Поделиться публикацией
Комментарии 72
  • +4
    Не скажу, что эта статья изменила мою жизнь, но она довольно близка к этому!
    • +5
      Годно, очень годно. На C# не пишу, но давно хотелось F# пощупать.
      • +3

        Отличная статья!
        Похоже, что это перевод этой публикации https://blogs.msdn.microsoft.com/dotnet/2017/07/24/get-started-with-f-as-a-c-developer/
        В результате F# и его преимущества становятся понятными и прозрачными!

        • +1
          Да, всё верно. Выше как раз указано, что это перевод, который сделали ребята из русского F#-сообщества.
          • +1
            Спасибо за ссылку на источник! Действительно стоящая статья на интересную тему.
          • +3

            F# превосходно подходит для описания доменной модели благодаря Discriminated Unions.
            Такой код даже выглядит более человекочитаемым для непрограммистов.


            На текущем месте работы я придерживаюсь такой схемы:


            • ядро, которое не ссылается на другие проекты — на F#. Вся логика, валидация, бизнес-кейсы и преобразования тут.
            • инфрастукрура и вся прочая обвязка (ASP.Net, Entity Framework, Akka.Net, работа с очередями) — на C#.

            Инфраструктуру проще на C# потому что:


            • F# пока что не поддерживает .Net Core
            • Ошибки и изменения в инфраструктуре случаются чаще, поэтому бОльшая часть разработчиков сможет исправить эту часть солюшна.

            Писать на F# это не элитизм ни разу. Пишется быстрее и ошибок в логике меньше. Рекомендую попробовать.


            Хороший пример DDD на F#.

            • +5
              Позволю себе поправить Вас. F# поддерживает .NET Core/Standard, но пока нет поддержки со стороны больших IDE — VS2017 и Rider. Надеюсь, что очень скоро ситуация изменится.

              Сейчас писать код под .Net Core можно в VSCode c плагином Ionide, а в .NET Core CLI есть шаблоны F# проектов.
              • +1
                F# пока что не поддерживает .Net Core

                Это утверждение не совсем точное: на самом деле F# поддерживает .NET Core (и вы можете в консоли создать и скомпилировать такой проект), а вот с инструментами (кроме Ionide) пока что не очень.


                • VSCode + Ionide в таком режиме работают отлично
                • Visual Studio пока не поддерживает, но обещали сделать в Update 3
                • Rider пока не поддерживает, но обещали сделать в одном из обновлений версии 2017 (т.е. в этом году)
                • про VS for Mac, к сожалению, не знаю

                В общем, хочется верить, что это временное явление, и в будущем основные IDE будут поддерживать всё как положено.

                • 0
                  Кстати, о поддержке в IDE. Сам я ими так и не научился пользоваться, пользуюсь vim почти без плугинов. Но в лекциях по Idris увидел потрясающие возможности IDE для типизированных языков — после описания сигнатуры большую часть работы по реализации делает плугин. Потом коллеги показали похожие фичи в IDEA для Scala — IDE очень ловко находила в контексте величины нужных типов (включая path depended) и подставляла их в кчестве аргументов функций. Система типов F# почти столь же мощная. IDE умеет испольховать эту мощь?
                  • +1

                    О, а это что за лекции такие?

                  • +1

                    Я думаю, что понимаю, о чём вы говорите, и поддержки такого уровня для F# пока что нет. Некий потенциал для такой поддержки на самом деле видится (как минимум, можно было бы заставить редактор генерировать ветки для pattern matching), но Idris всё-таки даёт для таких вещей намного больше возможностей благодаря куда более продвинутой системе типов.

                • 0
                  1. А почему акка не на F#?
                  2. Как преобразуете persistent-типы (EF) к доменным типам для передачи в F# BL?
                  • 0

                    упс, не сразу понял, что вопрос относится к посту уровнем выше, удалил комментарий

                    • +2

                      1) Да там смешение по большей части. Инфраструктуру с большим кол-вом мутабельного кода (кеши, стейты, вот это вот всё) проще на C#.


                      Очень много либ для Akka.Net написано не в функциональном стиле, поэтому для использования в F# какого-нибудь PersistentActor приходится огород городить. Ну а чтобы явно не смешивать два языка для одного фреймворка я просто пишу Akka код на C#, который уже лезет в бизнесовые объекты, сервисы etc в проект, написанный на F#.


                      2) Бьёте по-больному) Да, для перехода из C# в F# (и наоборот) приходится писать немного мапперов, т.к. Nullable в чистом F# надо заменять на option, например. И не пропускать null внутрь домена вообще.
                      А в остальном, RecordType из проекта F# в проекте на C# выглядят как класс с конструктором и понятными типами. Если надо хранить его в SQL, то для EF в любом случае надо лепить инфраструктурный класс с описанием индексов и пр. Я не сторонник смешивать доменные модели, DTO и модели для хранения.

                  • +1
                    А можно ли разобраться в F#, не изучая C#?
                    Конкретно меня интересуют два вопроса — как легко (то есть без установки IDE) писать свои модули к PowerShell, и можно ли использовать F# для AWS Lambda (сейчас для этого использую Scala, но приходится бороться с очень большим ростом размера бинарника — не получится ли то же самое с F# вместо C#)?
                    • 0
                      как легко (то есть без установки IDE) писать свои модули к PowerShell

                      Здесь принцип одинаков как для C#, так и для F#: нужны будут


                      • сборки от целевой версии PowerShell (Windows PowerShell хранит их в GAC, а для PowerShell Core можно всё скачать из NuGet)
                      • инструменты для сборки модулей (для Windows PowerShell, вероятно, понадобятся MSBuild и standalone-компилятор, которые можно попробовать установить без IDE вместе с VS Build Tools; для .NET Standard всё проще, потому что ставится из NuGet)

                      Я бы вам советовал самостоятельно попробовать написать простенький модуль на C#, а потом этот же опыт применить для разработки на F#.


                      Насколько я понимаю, сегодня есть рекомендация писать портабельные модули на .NET Standard, и их можно будет использовать и для старого, и для нового PowerShell. Это сильно упрощает задачу «писать модули без установки IDE», причём без привязки к языку.


                      Вот ещё нагуглился какой-то небольшой пост, который описывает опыт разработки модуля с .NET Core (без IDE). Опять же, повторюсь, история с F# принципиально не отличается; F# официально поддерживает .NET Core уже в течение некоторого времени.

                      • +1
                        Если не считать тонкостей взаимодействия с .NET, я знаю F# достаточно хорошо — благо не мало писал на OCaml и других функциональных языках. По этому учить C# мне не интересно, а вот познакомиться и использовать .NET из F# как раз очень интересно.
                    • +1
                      Оно, конечно, всё красиво и здорово, как и многие другие функциональные языки, и я неоднократно хотел изучить какой-нибудь поглубже, но натыкался на то, что не мог найти задачи, с которыми они бы справлялись радикально лучше того же шарпа с LINQ, лямбдами и профурсетками.

                      Какой может быть мотивация написать следующий проект именно на F# (Lisp, Haskell, %your_option%), а не на C#?
                      • +3

                        F# — это инструмент, а не религия. Как инструмент он больше подходит для определённого круга задач.
                        Для меня этот круг задач выглядит так:


                        • многопоточная молотилка данных
                        • доменные модели, ядро
                        • веб-сервис вида Pipeline (получил инпут, отдал аутпут)
                        • скрипты (билд скрипты для FAKE, Azure Functions и т.д.)

                        Мотивация в том, что F# умеет решать задачи из круга выше проще, быстрее и лучше.
                        Почему быстрее и лучше могу написать в отдельном посте.

                        • 0
                          Почему быстрее и лучше могу написать в отдельном посте.

                          Было бы интересно послушать. Напишите, пожалуйста :)

                          • +9

                            Каждый пункт из моего списка выполняется на F# проще из-за комбинации языковых фич F#, которые я перечислю ниже.


                            1) Type Inference + Automatic Generalization.
                            Поясню для C# разработчиков. У вас есть var, который позволяет "наследовать" (или правильнее сказать "вывести") тип для переменной слева из выражения справа. Представим что можно использовать var не только в выражениях объявления переменной, но и для объявления сигнатур функций.


                            При этом var != dynamic, т.е. мы не теряем сильную типизацию, мы просто говорим — пусть у функции будет такой выходной тип, который получается из тела функции. А входные параметры будут такого минимально необходимого типа, который требуется чтобы выполнить тело функции.


                            Подобное есть в C# в лямбдах, где можно написать (x => 1), где можно не указывать ни тип x (он будет унаследован из контекста применения лямбды), ни тип возврата (цифра 1 означает что возвращаемый тип int или его потомок). Но до размаха наследования типов и генерализации C# отстаёт на годы.


                            В F# можно писать функции без бойлерплейта в сигнатурах, типы за вас выведет компилятор. В начале они будут любыми дженериками, а затем он сам наложит констрейны на аргументы и выведет более чёткий интерфейс, тип или ограничения на операторы (дада, в F# можно наложить констрейн на возможность складывать).


                            2) Immutable Types + Record Types
                            Для начала попробуйте создать по-настоящему Immutable type в C#. Многие скажут что достаточно сделать один конструктор и кучу полей с приватными сеттерами. И рано или поздно попадутся на этом, т.к. ImmutableArray не является неизменяемым как только я получаю ссылку на объект MutableClass и начинаю его изменять как мне вздумается.

                            F# гарантирует трудности при написании такого кода :)
                            Зачем нужны Immutable типы, я думаю рассказывать не надо.


                            Так же, я уверен многие делали value object типы в C#, которые сравниваются по значению, а так же являются неизменяемыми. Да, решарпер берёт часть бойлерплейта на себя в виде переопределения GetHashCode, Equals и т.д. но каждый такой тип надо выделять в отдельный файл из-за безумного кол-ва бесполезного кода в нём.
                            Record Type в F# решает все эти проблемы разом и далее программист думает только о правильном создании доменной модели, а не о правильном переопределении GetHashCode


                            3) Создание DSL
                            F# умеет создавать инлайн операторы на лету для более выразительного кода.
                            let (>>=) f g x = {залогировать вызов и скомбинировать вызов функций}


                            пример (очень даже реальный:
                            getData >>= validate >>= transform >>= publish


                            А так же новые паттерны для паттерн матчинга:
                            match x with
                            | North ->…
                            | South ->…
                            etc


                            А так же свои монадные преобразования с помощью workflow синтаксиса.
                            maybe {
                            let! a = doDangerousOp1()
                            let! b = doDangerousOp2(a)
                            let! c = doDangerousOp2(b)
                            }
                            примерный смысл написанного выше — на каждом шаге операции оборачивать результат выражений в Maybe (это монада вида что-то есть или чего-то нет) и проверять, если в результате уже ничего нет, то ничего не делать. На выходе вернётся развёрнутая монада. Описывать сколько ж надо кода на c# написать для похожего функционала — страшно. Есть готовые библиотеки, но создание подобных монад в f# упрощено из-за встроенного в язык workflowBuilder. Собственно async или seq в F# — это и есть те самые workflow.


                            4) Dicriminated Union + Tuples
                            Широко применяемая фича языка — это DU. Это очень прокачанные enum из C#, которые могут иметь разные типы (а не только int), включать сами себя, быть generic и т.д.
                            Как правило именно они используются для описания работы приложения через описания возможных состояний, правил перехода, вида входных параметров и т.д. Комппилятор всегда подскажет что вы сделали не так с DU (например не рассмотрели все случаи), поэтому ошибится в логике сразу становится намного сложнее.


                            Про кортежи подробно рассказывать не буду. Просто скажу что в F# их применение выглядит естественно (в основном благодаря type inference) и поэтому используются повсеместо.


                            Можно описывать ещё кучу фич типа разнообразного кол-ва паттеров в паттерн матчинге, typeProviders (дают статическую типизацию к динамическому внешнему контенту) но лучще сразу перейду к основному.


                            Почему многопоточная молотилка?


                            • иммутабельность
                            • меньше бойлерплейта с value type

                            Почему доменные модели, ядро?


                            • DU описывают все возможные состояния системы. Сделать её неконсистентной при использовании DU — крайне сложно
                            • легко написать свой DSL с помощью своих операторов, монад и пр. вот пример:
                              let example =
                              trade {
                              buy 100 "IBM" Shares At Max 45
                              sell 40 "Sun" Shares At Min 24
                              buy 25 "CISCO" Shares At Max 56
                              }

                            Почему веб-сервисы вида pipeline?


                            • функциональные преобразования проще, т.к. меньше бойлерплейта, можно композировать функции, частично их применять, кода меньше.
                            • typeProviders позволяют быстро и без ошибок писать хоть SQL код, хоть обращаться к CSV, сохраняя типы и проверяя всё во время компиляции

                            Почему скрипты?


                            • Опять таки из-за легкого написания DSL получается человекочитаемый код даже для непосвящённых в F# девопсов или дата-аналитиков. Пример деплой билда на FAKE:

                            "Clean"
                            ==> "Build"
                            ==> "Deploy"


                            Ну и далее, скрипт можно править на лету тем же девопсам или аналитикам не погружаясь в F#.

                            • 0
                              1) Type Inference + Automatic Generalization.

                              Ну, с выводом типов в ФП не поспоришь, это круто. С другой стороны, в больших приложениях где используют шарп сигнатуры метода желательно видеть явно + можно случайно что-то поломать. Давно не писал на F#, пример из головы: взяли, поменяли что-то в теле функции, тип int поменялся на string и получили ошибки во всех местах вызова, хотя хотелось бы получить одну ошибку, что возвращаемое значение не соответствует декларируемому типу. В лямбдах это не проблема, потому что они обычно не переиспользуются, хотя и там циклически идет вывод типа и карсным подчеркивается пол-экрана. Тут же ситуация усугубляется. Вывод нелокальных типов был специально так задуман и реализован, как с точки зрения производительности компилятора, так и с точки зрения того, чтобы ошибка не ускакивала за 10 слоев абстракции от того места, где ты реально накосячил.


                              2) Immutable Types + Record Types
                              Что такое "по-настоящему Immutable" довольно холиварная вещь. Можно посмотреть выступление Скита на дотнексте на эту тему, довольно занятно. В любом случае, сделать Immutable-types в шарпе спокойно можно, типичный пример — структура с get-only свойствами. Но работать с ними больнее, это конечно никто не спорит. Иммутабельность в ФП очевидно будет лучше, чем в C-like ООП языке.

                              3) Создание DSL
                              F# умеет создавать инлайн операторы на лету для более выразительного кода.

                              Ну, тут и спорить не о чем, фича очень полезная.


                              А так же новые паттерны для паттерн матчинга:

                              Паттерн матчинг плохенький завезли в C# 7.0. В 8.0 должен появиться помощнее + он должен быть expression, а пока что обходимся statement'ом. В итоге в некотором виде есть уже.


                              А так же свои монадные преобразования с помощью workflow синтаксиса.

                              Не вопрос и в шарпе
                              ```сsharp
                              var c = doDangerousOp1()?.. doDangerousOp2()?.doDangerousOp2();


                              Не так мощно, но 99,9% потребностей покрывается.
                              
                              > 4) Dicriminated Union + Tuples
                              
                              Таплы есть и в шарпе (не те, которые System.Tuple, а нормальный), ну а DU мб завезут когда-нибудь...
                              
                              Скрипты имхо идеально пишутся на powershell, тут F# не сильно выигрывает у C#. 
                              
                              Лично мой опыт с F# что без мутабельных операций некоторые вещи очень сложно писать. Причем как раз-таки алгоритмы придуманные в 50-80 годах, которые описываются как "а теперь сюда перезапишем вон то, а потом и вон то". И либо приходится мутировать переменны и быть редиской и "грязным" парнем, либо очень сильно извращаться. Я пошел по второму пути (потому что иначе зачем писать на ФП и на каждом шагу его хачить?) и в итоге забил. Но на шарпе стал намного лучше и дженериками пользоваться, и лямбдами.
                              
                              В итоге возможно я F# немного не понял, но во многом он выглядит как более мощный шарп (который постепенно у него фичи и ворует), но на котором писать сложнее во многих случаях. Опять же, может я просто не умею его готовить.
                              • 0
                                Давно не писал на F#, пример из головы: взяли, поменяли что-то в теле функции, тип int поменялся на string и получили ошибки во всех местах вызова, хотя хотелось бы получить одну ошибку, что возвращаемое значение не соответствует декларируемому типу.

                                А если это не ошибка, а какой-нибудь глобальный рефакторинг? Тогда как раз удобно будет во всех местах вызова исправлять.
                                • 0
                                  1. В большинстве случаев это ошибка, а язык должен покрывать основные сценарии использования, а не крайне редкие. В Хаскелле поэтому границы вывода ограничены именно по этой причине, чтобы ошибка не утекала в какое-то левое место. Очень неприятно 2 часа сидеть искать, где же развалилось. Не сильно удобнее, чем читать 300-страничные сообщения об ошибке в плюсовых темлейтах.
                                  2. Если вдруг это глобальный рефакторинг, достаточно заменить слово int на string в одном месте и получить желаемый результат. Причем упадут вызовы этой функции, а не вызовы вызовы вызовов. А у нас получается, с полным выводом типов упадет самая верхняя функция, вернувшая неподходящий тип, а не самая близкая к месту реального изменения
                                  • 0

                                    В хаскеле границы вывода не особо ограничены, у вас вполне могут быть top-level-сигнатуры без аннотаций типов.

                                    • +1
                                      Кину известную цитату, которая собственно мне нравится в плане доходчивости (Про Хиндли-миндлер речь):
                                      тем, что алгоритм устроен так, что место ошибки определить невозможно

                                      хм строит систему из пицотыщ уравнений и потом начинает ее решать унификацией

                                      то есть он берет какоето уравнение, подставляет туда абстрактный аргумент, по нему подставляет в другое у-е и т.д. — в каком-то у-е на аругмент накладывается ограничение, аргумент становится более конкретным
                                      если где-то ошибка — то В НЕКОТОРОМ месте в графе возникнет противоречие — будет наложено ограничение, которому аргумент уже не может удовлетворить (из-за другого ограничения, которое было наложено раньше, например сперва сказали, что должна быть строка, а потом — что число, при этом неизвестно где именно ошибка — там где фиксирована строка, или там где фиксировано число, и в зависимости от хода реализации алгоритма он выведет ошибку или в том месте или в другом)

                                      и ты бай дезигн не можешь указать, в какой точке графа ошибка — в какой-то
                                      в любой
                                      ты можешь только сказать, что «вот в этом месте мой вывод обломался», при чем для одного и того же кода то место, в котором он обломается, зависит от деталей реализации алгоритма, и при малейших вариациях кода — оно может прыгать через десятки файлов, сквозь тыщи строк кода

                                      хачкелисты решают эту проблему например так, что считают отсутствие типов у топлевел ф-й кодесмеллом — если типы конкретные для ф-и указаны, то граф фиксируется на границе ф-и и значит ошибка точно внутри, алгоритм гарантированно обломается при пересечении границы ф-и (если ошибка в ней)
                                      при это офк твоя ошибка может быть где-то в середине ф-и, а проблему тебе покажет в том, что возвращаемое значение не соответствует (например)
                                      но это все же не выходит уже за пределы юзабельности
                                      • 0

                                        Ну да, кодесмелл, и ghc пишет ворнинг с -Wall, если у вас вдруг топ-левел-байндинг не имеет сигнатуры. Но это не ограничение языка, а, скажем так, best practices.

                                        • 0
                                          Если язык не форсирует соблюдение best practice — это скорее минус, а не плюс :)
                                          • 0

                                            Он и так своей типизацией много чего форсирует, оставьте же что-нибудь для раздолбайства!


                                            А вообще удобно, скажем, в тестах, где эти топ-левел-сигнатуры только зашумляют всё, написать


                                            {-# OPTIONS_GHC -Wno-missing-signatures #-}

                                            и радоваться жизни.

                        • +5

                          Отвечу про F#. Я уже в течение некоторого времени решаю некоторые научные и инженерные задачи с помощью F#, и могу сказать следующее: F# мешает писать приложение неправильно, и в этом его основное преимущество.


                          F# мешает вам использовать мутабельные переменные (синтаксис для этого есть, но его надо вспоминать, и выглядит громоздко). F# мешает вам делать запутанную многостороннюю архитектуру в проекте благодаря линейному порядку файлов в проекте и определений внутри файла (есть синтаксис для того, чтобы сделать нелинейный порядок, но выглядит это некрасиво и его нужно вспоминать). F# мешает вам сделать у объекта несколько конструкторов, и тем самым мешает делать большие объекты, нарушающие SRP (ну вы поняли, да, синтаксис для этого есть, но вспоминать его редко когда хочется). F# мешает вам перепутать единицы измерения в физической формуле (или загнать туда неопределённые единицы) после того, как вы начинаете их использовать в программе.


                          Мне он напоминает мудрого наставника, который указывает мне недостатки программы ещё во время написания кода. И это очень, очень удобно.


                          Ну и чуть меньшее преимущество F# заключается уже в том, что он помогает писать программы в функцональном стиле, с применением операторов типа |>, композиции функций, паттерн-матчинга.

                          • +2
                            F# мешает писать приложение неправильно, и в этом его основное преимущество.

                            Это только мне Delphi (и Pascal) вспомнился?

                            • 0
                              Как бы Вирт эту цель перед собой ставил. Но достичь ее, IMHO, не удалось. Писать неправильно они почти не мешали, но и писать правильно не слишком помогали.
                          • +2
                            Не обязательно проект писать, но могу про провайдер типов SQLProvider для баз данных совсем коротко рассказать. При его использовании не надо писать кучу boilerplate кода в виде Entity классов т.к. типы выводятся автоматически относительно базы данных еще на этапе написания кода, что позволяет как минимум некоторые мелкие «скриптовые» задачи сделать несколько быстрее. Плюс через него же можно LINQ запросы писать.

                            Еще есть CsvProvider, смысл примерно тот же: выводятся типы для каждого поля csv.

                            Минус: это пока не работает на .NET Core.
                          • +3

                            F# применяю лет 5-6 для внутренней аналитики, DSL, простых poore-IO сервисов и бизнес логики по причинам, упомянутым Szer. Например F# у меня удачно вписывается в создание вью-моделей WPF в связке с вивером Fody.PropertyChanged, который бесплатно даёт F#-рекордам с mutable полями наследование IPropertyChanged. Весьма удобная опция для доменных модели WPF/UWP приложений, где F# имеет много шансов сиять.


                            Вообще по впечатлениям F# простой и практичный язык. В качестве не недостатков, но расстановки точек над И для тех кто планирует в F# отмечу


                            • как функциональный язык F# не поддерживает (или поддерживает ограниченно ) многие хайтек парадигмы ФП из Haskel/Erldng, что имеет как плюсы так и минусы.


                            • иногда при программирования на F# я сталкивался с тем, что мне требовался бОльший контроль над низкоуровневыми частями системы, чем может предоставить язык. В этом случае приходилось делать полный или частичный откат в C#. После 2-3 кейсов я был готов к такому варианту развития событий и особых проблем не возникало, благо C# и F# имеют много общего.
                            • +1
                              как функциональный язык F# не поддерживает (или поддерживает ограниченно ) многие хайтек парадигмы ФП из Haskel/Erldng

                              Правильно ли я понимаю, что


                              • зная C# можно сначала перейти на F#
                              • а затем изучив F# можно перейти на Haskel или Erlang?
                              • +2

                                Знание C# (из чего автоматом следует знание инфраструктуры .Net) сильно поможет освоить F# — можно сразу переходить от теории к практике.


                                Изучить Haskell определённо проще, зная F#, поскольку оба языка из семейства ML и предполагают те же функциональные концепты — каррирование аргументов, неизменяемость, пайплайнинг, ФВП, cписковые включения, построители выражений (do-нотация Haskell, вычислительные выражения F#). Но шагов от F# до Haskell предстоит сделать на много больше, чем от C# к F#. Ленивость по умолчанию и классы типов делают Haskell значительно более сложным для изучения, чем F#


                                Имхо F# не много даст для практического освоения Erlang, динамического языка со специфическим синтаксисом, если вообще что-то даст. Слишком сильно языки и среды выполнения отличаются.

                                • +1

                                  Спасибо за разъяснения!

                            • +2

                              А какие есть ORM для использования с F#? И как они поддерживают discriminated unions?

                              • +1

                                Отвечаю на первую половину вашего вопроса: для F# можно пробовать использовать те же ORM, что и для C#, но без поддержки discriminated unions. Вот тут есть довольно старая статья, по которой можно примерно оценить, с какими сложностями предстоит столкнуться.


                                А по второй половине: в F# распространена парадигма работы с БД, отличная от использования обычных ORM, к которым мы с вами привыкли по C#. Тут часто используют т.н. провайдеры типов. Вот руководство от Microsoft, которое описывает работу с FSharp.Data.TypeProviders. Есть и другие провайдеры, которые могут вас заинтересовать: SqlClient, SQLProvider.


                                Выше тов. Liminiens это тоже упоминал.

                                • +2

                                  Немного дополню.


                                  Руководство от Microsoft подходит для ознакомления с провайдерами типов, однако использовать FSharp.Data.TypeProviders не рекомендуется, эта библиотека устарела. Чаще всего используют упомянутый SQLProvider. Я недавно встретил пару неплохих статей о том, как это работает в Linux под Mono:



                                  Следует еще упомянуть про Rezoom.SQL. Это новый провайдер типов для SQL, обладающий прекрасной документацией.


                                  В этой истории есть ложка дегтя. К сожалению, провайдеры типов пока не работают под .NET Core. Поэтому приходится использовать стандартные решения, например, Dapper.

                                  • +1
                                    Вместо Dapper можно использовать Natty. По сути данная библиотека является обёрткой над Slapper, но со поддержкой F# Records и Option types. Ещё из особенностей, возможность использовать в запросах sprintf like синтаксис с статической проверкой типов и писать комбинированные запросы (собирать один запрос из нескольких других)
                              • +1
                                Пишу на C# уже более 14 лет, ходил вокруг, да около F#, но как-то он не зашел.
                                То ли проекты неподходящие, то ли я его не воспринимал… Да и с интеграцией F# и С# проекта есть определённые сложности. А с развитием C# и вовсе его «функциональщины» стало хватать.
                                Но, на текущей работе вдруг понадобилась разработка для Apache Spark, и… открыл для себя Scala, и в принципе, воспринял :)
                                Хотя от Java и всего с ней связанного ранее воротило…
                                Не могу сказать, что Scala лучше F# или хуже, но синтаксис F# сложнее, что-ли.
                                Например, let add x y = x + y, на Scala можно написать и в более привычном (для C#) виде def add(x:Int,y:Int) = x + y. Да, F# запись проще, но не цепляет, приходится вглядываться.
                                Мнение сугубо личное и, возможно, после более длительного знакомства со Scala я пересмотрю его и F# мне тоже понравится…
                                • +1
                                  Например, let add x y = x + y, на Scala можно написать и в более привычном (для C#) виде def add(x:Int,y:Int) = x + y. Да, F# запись проще, но не цепляет, приходится вглядываться.

                                  на F# можно и так, и вглядываться не надо:
                                  let add (x: int, y: int) = x + y


                                  Только это не нужно, т.к. VS Code над такой функцией напишет int -> int -> int:
                                  let add x y = x + y


                                  Так что это скорее в плюс F#

                                  • 0
                                    Но это же функции разного совершенно типа, одна каррированная, а вторая нет.
                                    • 0

                                      В примере на Scala тоже показали не каррированную.
                                      (но замечание справедливое)

                                      • +1
                                        Да, я почему-то сначала не обратила внимание на это, а когда появился ответ, тогда и там заметила.

                                        В общем, написать-то можно и так, и так. Главное не забывать, что код получается не эквивалентный.
                                        • +1

                                          Я специально так сделал, чтобы усилить эффект похожести.
                                          int->int->int
                                          let add (x: int) (y: int) = x + y
                                          int * int->int
                                          let add (x: int, y: int) = x + y

                                  • +1

                                    image

                                  • 0

                                    ошибся веткой

                                    • 0
                                      Было бы полезно писать пример на C# рядом, чтобы было с чем сравнить. Раз уж это пособие для разработчиков.
                                      Для меня основная проблема F# я не понимаю что будет внутри в IL и какая будет производительность этого всего + как влепить туда ограничения.

                                      Например тот же пример суммы квадратов, что будет если дать инпут который вернет больше int.MaxValue или long.MaxValue?

                                      например:
                                      let square x = x * x
                                      let sumOfSquares n =
                                      [1..n] // Создадим список с элементами от 1 до n
                                      |> List.map square // Возведем в квадрат каждый элемент
                                      |> List.sum // Просуммируем их!
                                      printfn «Сумма квадратов первых 5 натуральных чисел равна %d» (sumOfSquares 5)
                                      printfn «Сумма квадратов первых 100000000000 натуральных чисел равна %d» (sumOfSquares 100000000000)

                                      что покажет компилятор для типа sumOfSquares?

                                      Ниже ваши же примеры на C# (кроме типов, не могу сейчас это переписать).
                                      Это можно копирнуть как в console app, так и в C#Script .csx если вам хочется ближе к интерпретатору.

                                      int square(int x) => x * x;
                                      int sumOfSquares(int n) =>
                                      Enumerable.Range(1, n) // Создадим список с элементами от 1 до n
                                      .Select(square) // Возведем в квадрат каждый элемент
                                      .Sum(); // Просуммируем их!
                                      void Sample1() =>
                                      Console.WriteLine($«Сумма квадратов первых 5 натуральных чисел равна {sumOfSquares(5)}»);

                                      // 'getMessage' — это функция, и `name` — ее входной параметр.
                                      string getMessage(string name) => name == «Phillip»? «Hello, Phillip!»: «Hello, other person!»;
                                      string phillipMessage => getMessage(«Phillip»); // getMessage, при вызове, является выражением. Его значение связано с именем 'phillipMessage'.
                                      string alfMessage => getMessage(«Alf»); // Это выражение связано с именем 'alfMessage'!

                                      // Создадим список квадратов первых 100 натуральных чисел
                                      List first100Squares => Enumerable.Range(1, 100).Select(x => x * x).ToList();
                                      // То же самое, но массив!
                                      int[] first100SquaresArray => Enumerable.Range(1, 100).Select(x => x * x).ToArray();

                                      // Функция, которая генерирует бесконечную последовательность нечетных чисел
                                      //
                                      // Вызывать вместе с Seq.take!
                                      bool isOdd(int i) => i % 2 != 0;
                                      IEnumerable odds() => Enumerable.Range(1, int.MaxValue).Where(isOdd);
                                      void printfn() => Console.WriteLine($«Первые 3 нечетных числа: [{string.Join(»; ", odds().Take(3))}]");
                                      // Вывод: «Первые 3 нечетных числа: seq [1; 3; 5]

                                      IEnumerable getOddSquares(IEnumerable items) =>
                                      items
                                      .Where(isOdd)
                                      .Select(square);
                                      • +1

                                        Даже с тривиальными примерами Вам пришлось выводить типы явно, руками.
                                        Попробуйте эти 35 строчек на C# изобразить
                                        Картинка
                                        Gist


                                        Есть мнение что на втором же методе случится затык.

                                        • 0
                                          Так я этот пост открыл, потому что хотел как C# программист понять как что-то что я знаю будет выглядеть в чем-то что я не знаю.
                                          Я не могу конвертнуть ваши 35 строчек, потому что я не читаю F# даже со словарём и понятия не имею что такое
                                          let inline
                                          или
                                          static member (+) (a, b)
                                          • +1
                                            также я не понимаю что значит
                                            1) a.Names @ b.Names
                                            2) x.SalarySum / decimal x.Names.Length
                                            3) Ну и как раз про типы было бы удобно видя примеры сайд бай сайд сравнить что проще.
                                            Я уже давно не задаюсь вопрос «как легко что-либо написать», основные вопросы на нормальном уровне приложений «как легко это будет понять если не трогать это год» «как легко это будет понять другому девелоперу» «как легко это будет поменять».

                                            4) По поводу неявных типов к вашему джисту у меня сразу как у C# девелопера вопросы:
                                            let genericMapReduce toOutput folder src
                                            folder это string или System.IO.DirectoryInfo? как это должен понять девелопер, который читает этот код на гитхабе?

                                            5) каким образом можно понять что в этом примере
                                            let toReport person =
                                            { Names = [person.Name]
                                            SalarySum = person.Salary }

                                            person это type Person и соотв. F# даёт использовать .Name, .Salary?

                                            6) вот это похоже на то же самое что в C#
                                            Seq.mapi (fun i n ->
                                            { Name = n; Salary = decimal (i * 100) })
                                            Я так понимаю F# считает что i это int иначе зачем писать decimal(i*100)

                                            Кстати попозже мб попробую написать это в C# 7, с использованием кортежей. Вполне может быть выйдет столько же букв.
                                            • +1

                                              Я тоже не пишу на F#, но разобраться в коде элементарно.


                                              1. let inline — подсказка компилятору, что надо встраивать метод
                                              2. static member (+) (a, b) — в C# это operator+
                                              3. a.Names @ b.Names — из контекста: результат конкатенации списков. (Concat в C#)
                                              4. x.SalarySum / decimal x.Names.Length — приведение типов

                                              Единственная проблема при переносе на C# в том, что он не поддерживает ограничение (+) в дженериках.
                                              Приведенный пример больше напоминает шаблоны С++.
                                              Вопрос: можно ли mapReduceAdd использовать в другом модуле?

                                              • 0
                                                1. это [MethodImpl]? зачем он в этом месте в F#? C# сам это сделает без подсказок в билде с оптимизациями
                                                2. я тоже так подумал, но это ппц такое делать в реальном коде, и уж тем более как пример (для классов типа PersonReport)
                                                3. a.Names b.Names это List, т.е. @ это a.Union(b).ToList() или без ToList()?
                                                4. это сделано чтобы дать hint типу cachedSum чтобы делать деление без округления? C# скомпилится и так и так.
                                                • 0
                                                  1. Не совсем для оптимизации. Особенность компилятора при работе с дженериками. Не стоит на этом заморачиваться, это не главное
                                                  2. Это обычное дело при работе с моноидными типами и агрегацией большого кол-ва данных в разных срезах.
                                                  3. List в F# это действительно список (в C# List — это массив вообще-то), но односвязный.
                                                    Конкатенация списков в F# почти бесплатная (ссылку у последнего элемента подменить, причём для этого ему не надо проходить один из списков до конца из-за внутренней оптимизации). Полный аналог в C# — это работа с LinkedList, но для данной цели это тоже неважно, можно и в массивах.
                                                  4. F# не допускает неявных преобразований, даже из int в decimal. Их можно сделать, но по умолчанию их нет. В C# их тоже можно сделать (implicit operator), но по умолчанию они есть.
                                                  • 0
                                                    1. все же интересно что за особенность — именно из-за этих особенностей как раз кому то кажется что C# «громоздкий» и F# этим лучше.
                                                    2. мб, как C# девелопер понятия не имею что такое моноидный тип
                                                    3. они мутабл или иммутабл?
                                                    если написать
                                                    let a (PersonReport)
                                                    let b (PersonReport)
                                                    let c (PersonReport)
                                                    let ab = a+b
                                                    let abc = ab+c

                                                    что будет в ab.Names?
                                                    4. C# не делает преобразований когда ничего не указывается, он возвращает округленный int, когда один из аргументов кастуется в decimal то возвращается decimal. в IL будет разный код для деления интов и деления децималов и будет разный перформанс этих операций.
                                                    чтобы было понятнее 1 / 2 это так сказать Int32.Divide(1, 2)
                                                    1m / 2 это decimal.Divide(1, 2)
                                                    • 0
                                                      1. Это очень большая тема, но именно она (с примерами на C#) изложена здесь.
                                                        Вкратце — в IL нет констрейна на (+), поэтому мы не можем скомпилировать отдельную функцию с таким ограничением. Но можно изхитриться и не компилировать такую "функцию" отдельно, а воспользоваться инлайнингом и разрешать проблемы с типами в контексте CallSite.
                                                        Именно это и позволяет делать F#. Эти дополнительные констрейны, которых нет в C#, не магия и не костыль, а всего лишь хитрое использования инлайнинга.


                                                      2. Это из теории категорий. Пример — у вас триллион твитов и вам надо по нажатию кнопочки сформировать срез "среднее кол-во лайков".
                                                        Сама по себе сущность tweet — неагрегируема, да и слишком много там ненужной инфы. Поэтому мы создаём дополнительный тип, который содержит нужные нам поля для агрегации и является агрегируемым.
                                                        Затем мы преобразуем tweet в новую сущность (map)
                                                        И агрегируем новые сущности (reduce)
                                                        А чтобы всё это работало в кластере с возможностью разбиения на чанки и инкрементальным добавлением к уже сагрегированым данным новой инфы надо добавить ещё пару ограничений:
                                                        1) операция агрегации должна быть ассоциативна — (a+b)+c=a+(b+c)
                                                        2) тип для агрегации должен иметь некий Zero элемент: Z+a=a, a+Z=a.
                                                        И тогда внезапно выясняется что это моноид!


                                                      3. Списки в F# иммутабл
                                                        В ab.Names будут два имени. Но сами значения, которые содержатся в элементах списка заново аллоцироваться не будут.
                                                        Пример с комментариями


                                                      4. 1m / 2 — это деление явного decimal на явный int, который почему-то преобразуется в decimal.Divide(). А почему не в Int32.Divide()?
                                                      • 0
                                                        1. интерестинг
                                                        2. ну это тот же .Select + .Aggregate с несколько большим количеством сахара?
                                                        3. по примеру интересно что будет после 21-й строки в
                                                        abList.Tail.Tail.Head.Equals ( c )
                                                        compile error, runtime error?
                                                        если abList не меняется, то получается всё же что референсы на элементы abList копируются и abList.Tail != abcList.Tail
                                                        т.е. это типичный List where T: class, но не List where T: struct.

                                                        Что опять же не отвечает на что такое @ — .Union() (=итератор) или .Union().ToList() (=копии референсов на все элементы обоих списков)

                                                        4. да согласен в этом случае, int приводится в decimal автоматически.
                                                        • 0

                                                          2) Так точно. Всё то же самое в С# можно сделать.
                                                          Select -> map
                                                          Aggregate -> fold
                                                          аналога reduce в C# нет, но это тот же fold, но без указания начального стейта (т.е. мы сразу агрегируем данные, без нулевого элемента)


                                                          3) abList.Tail.Tail вернёт пустой список, поэтому abList.Tail.Tail.Head даст рантайм ошибку — System.InvalidOperationException: The input list was empty.

                                                • 0
                                                  можно ли mapReduceAdd использовать в другом модуле?

                                                  Да, конечно. Он максимально generic.
                                                  На C# его сигнатура выглядит так:


                                                  TOutput mapReduceAdd <TInput, TOutput, TSource> 
                                                      (Func<TInput, TOutput> toOutput, TSource src)
                                                          where TOutput: + //да, в C# так не получится
                                                          where TSource: IEnumerable<TInput>
                                                  • +1
                                                    на C# напишешь where TOutput: IConcatenatable
                                                    и на классе вместо
                                                    public static operator +

                                                    будет IEnumerable/IList/T[]/LinkedList Concatenate(...)
                                                    смотря что хочется
                                                    • +1

                                                      Да, согласен. Но на F# это даже писать не придётся, компилятор сам выведет :)

                                          • 0
                                            Для меня основная проблема F# я не понимаю что будет внутри в IL и какая будет производительность этого всего + как влепить туда ограничения.

                                            В большинстве случаев производительность будет страдать. При применении различных идиом ФП деградация перформанса практически неизбежна. Насколько это допустимо и оправдано бонусами, которые даёт F#, надеюсь сами понимаете — зависит от задачи. Если опыт и интуиция подсказывают вам, что код может стать узким местом — не задумываясь пишите его на C#.


                                            Но тут надо сказать, что автор F# очень много сделал чтобы деградация перформанса была минимально возможной и всегда об этом много пёкся. Например, рекорды в F# не плоские как haskell, а древовидные в b-tree. Что делает их весьма полезными персистентными структурами данных в приложениях с интенсивным IO


                                            дать инпут который вернет больше int.MaxValue или long.MaxValue?

                                            будут ошибки компиляции что-то вроде "число вне допустимого диапазона" и "надо int64, а получен long" соотв. F# очень строго следит за такими вещами и никаких глупых казусов со слабыми неявными преобразованиями чисел не допустит. Даже по мнению некоторых программистов — слишком строго)


                                            sumOfSquares

                                            Это у них крайне не удачный пример. Я бы на вашем месте написал на С# следующим образом


                                            static int SumOfSquaresOfNormalHuman(int n)
                                            {
                                                var a = 0;
                                                for (int i=0; i<n; i++)
                                                {
                                                    a += i * i;
                                                }
                                                return a;
                                            }

                                            Подобные глупости с якобы "учебными" примерами очень сильно дискредитируют F# в глазах опытных инженеров C#. У последних сразу возникает ощущение впаривания фуфла.

                                            • 0
                                              не очень понимаю как компилятор следит,
                                              т.е. если у меня есть функции
                                              sum a, b => a+b;
                                              deduct a, b => a-b;

                                              то F# скажет
                                              OK sum 1 2
                                              COMPILE ERROR sum int.MaxValue-1 2
                                              OK deduct 2 1
                                              COMPILE ERROR deduct int.MinValue+1 2

                                              Так он это делает?
                                          • +1
                                            Благодарю за пост. Последнее время считал F# чем-то начавшимся за здравие в одно время, но погибшим из-за непопулярности. Все же после прочтения статьи и посещения ссылок, остался неприятный осадочек, что скоро могут прикрыть поддержку языка.
                                            • +3

                                              К счастью, никто не может "прикрыть" F#. Язык поддерживается сообществом и F# Software Foundation, исходный код компилятора открыт под лицензией Apache 2.0. Насколько мне известно, компания Microsoft также не собирается отказываться от поддержки F# в Visual Studio.

                                              • +2

                                                Планы Microsoft относительно F# изложены в The .NET Language Strategy.

                                                • +1
                                                  У нас также был перевод этой статьи.
                                                  • +1

                                                    Если перевод "Get Started with F# as a C# developer" очень хорош (на мой взгляд, взаимозаменяем с оригиналом), то, к сожалению, перевод "The .NET Language Strategy" огорчает.


                                                    Очень похоже на машинный перевод, или дословный перевод человеком, далеким от разработки:
                                                    "подписи API" — в оригинале это "API's signatures", т.е., сигнатуры, декларации, заголовки API, но никак не подписи;
                                                    и множество подобных моментов.

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

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