Processor=Intel Core i5-3450 CPU 3.10GHz (Ivy Bridge), ProcessorCount=4
Есть предложение лучше — прогнать с BenchmarkDotNet: F# — у меня выдаёт 56.29 ms (я снизил кол-во итераций в коде до 100к) C# — у меня выдаёт 355.1 ms
Попробуйте запустить тот же код в релизе (я собирал всё под 4.6.1, т.к. ParallelExtensionsExtra под netcore нет)
Подозреваю что есть ещё что-то, т.к. я точно использовал оптимизированный пример.
Учитывая такую непостоянность результатов, нельзя сказать что они у кого-то из нас достоверные.
2) Так точно. Всё то же самое в С# можно сделать.
Select -> map
Aggregate -> fold
аналога reduce в C# нет, но это тот же fold, но без указания начального стейта (т.е. мы сразу агрегируем данные, без нулевого элемента)
3) abList.Tail.Tail вернёт пустой список, поэтому abList.Tail.Tail.Head даст рантайм ошибку — System.InvalidOperationException: The input list was empty.
Это очень большая тема, но именно она (с примерами на C#) изложена здесь.
Вкратце — в IL нет констрейна на (+), поэтому мы не можем скомпилировать отдельную функцию с таким ограничением. Но можно изхитриться и не компилировать такую "функцию" отдельно, а воспользоваться инлайнингом и разрешать проблемы с типами в контексте CallSite.
Именно это и позволяет делать F#. Эти дополнительные констрейны, которых нет в C#, не магия и не костыль, а всего лишь хитрое использования инлайнинга.
Это из теории категорий. Пример — у вас триллион твитов и вам надо по нажатию кнопочки сформировать срез "среднее кол-во лайков".
Сама по себе сущность tweet — неагрегируема, да и слишком много там ненужной инфы. Поэтому мы создаём дополнительный тип, который содержит нужные нам поля для агрегации и является агрегируемым.
Затем мы преобразуем tweet в новую сущность (map)
И агрегируем новые сущности (reduce)
А чтобы всё это работало в кластере с возможностью разбиения на чанки и инкрементальным добавлением к уже сагрегированым данным новой инфы надо добавить ещё пару ограничений:
1) операция агрегации должна быть ассоциативна — (a+b)+c=a+(b+c)
2) тип для агрегации должен иметь некий Zero элемент: Z+a=a, a+Z=a.
И тогда внезапно выясняется что это моноид!
Списки в F# иммутабл
В ab.Names будут два имени. Но сами значения, которые содержатся в элементах списка заново аллоцироваться не будут. Пример с комментариями
1m / 2 — это деление явного decimal на явный int, который почему-то преобразуется в decimal.Divide(). А почему не в Int32.Divide()?
Не совсем для оптимизации. Особенность компилятора при работе с дженериками. Не стоит на этом заморачиваться, это не главное
Это обычное дело при работе с моноидными типами и агрегацией большого кол-ва данных в разных срезах.
List в F# это действительно список (в C# List — это массив вообще-то), но односвязный.
Конкатенация списков в F# почти бесплатная (ссылку у последнего элемента подменить, причём для этого ему не надо проходить один из списков до конца из-за внутренней оптимизации). Полный аналог в C# — это работа с LinkedList, но для данной цели это тоже неважно, можно и в массивах.
F# не допускает неявных преобразований, даже из int в decimal. Их можно сделать, но по умолчанию их нет. В C# их тоже можно сделать (implicit operator), но по умолчанию они есть.
можно ли mapReduceAdd использовать в другом модуле?
Да, конечно. Он максимально generic.
На C# его сигнатура выглядит так:
TOutput mapReduceAdd <TInput, TOutput, TSource>
(Func<TInput, TOutput> toOutput, TSource src)
where TOutput: + //да, в C# так не получится
where TSource: IEnumerable<TInput>
Я специально так сделал, чтобы усилить эффект похожести.
int->int->int let add (x: int) (y: int) = x + y
int * int->int let add (x: int, y: int) = x + y
Например, 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
1) Да там смешение по большей части. Инфраструктуру с большим кол-вом мутабельного кода (кеши, стейты, вот это вот всё) проще на C#.
Очень много либ для Akka.Net написано не в функциональном стиле, поэтому для использования в F# какого-нибудь PersistentActor приходится огород городить. Ну а чтобы явно не смешивать два языка для одного фреймворка я просто пишу Akka код на C#, который уже лезет в бизнесовые объекты, сервисы etc в проект, написанный на F#.
2) Бьёте по-больному) Да, для перехода из C# в F# (и наоборот) приходится писать немного мапперов, т.к. Nullable в чистом F# надо заменять на option, например. И не пропускать null внутрь домена вообще.
А в остальном, RecordType из проекта F# в проекте на C# выглядят как класс с конструктором и понятными типами. Если надо хранить его в SQL, то для EF в любом случае надо лепить инфраструктурный класс с описанием индексов и пр. Я не сторонник смешивать доменные модели, DTO и модели для хранения.
Каждый пункт из моего списка выполняется на 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#.
F# превосходно подходит для описания доменной модели благодаря Discriminated Unions.
Такой код даже выглядит более человекочитаемым для непрограммистов.
На текущем месте работы я придерживаюсь такой схемы:
ядро, которое не ссылается на другие проекты — на F#. Вся логика, валидация, бизнес-кейсы и преобразования тут.
инфрастукрура и вся прочая обвязка (ASP.Net, Entity Framework, Akka.Net, работа с очередями) — на C#.
Инфраструктуру проще на C# потому что:
F# пока что не поддерживает .Net Core
Ошибки и изменения в инфраструктуре случаются чаще, поэтому бОльшая часть разработчиков сможет исправить эту часть солюшна.
Писать на F# это не элитизм ни разу. Пишется быстрее и ошибок в логике меньше. Рекомендую попробовать.
Спасибо за результаты. Я правда не знаю о чём это говорит.
Могу только предположить что asyncWorkflow в F# менее оверхедный чем Task в C#.
Processor=Intel Core i5-3450 CPU 3.10GHz (Ivy Bridge), ProcessorCount=4
Есть предложение лучше — прогнать с BenchmarkDotNet:
F# — у меня выдаёт 56.29 ms (я снизил кол-во итераций в коде до 100к)
C# — у меня выдаёт 355.1 ms
Попробуйте запустить тот же код в релизе (я собирал всё под 4.6.1, т.к. ParallelExtensionsExtra под netcore нет)
Подозреваю что есть ещё что-то, т.к. я точно использовал оптимизированный пример.
Учитывая такую непостоянность результатов, нельзя сказать что они у кого-то из нас достоверные.
Ссылка на аплоад dll битая. Вот верная
Я запустил Ваш оптимизированный через
StaTaskScheduler
код и он выдал ~1300мс.Я набросал то же самое на F# ~700мс.
Код
Картинка из консоли.
Первый запуск — релизная сборка Вашей версии, второй запуск — моей.
Ссылка на .Net dll + pdb
Возможно я что-то сделал не так если разница аж в два раза между языками одной и той же платформы.
P.S. я там в for ошибся, мой код на 1 итерацию больше делает :)
Если хочется продвигаться в карьере и расти в з/п — надо постоянно учиться.
"Пока ты спишь, кто-то качается".
2) Так точно. Всё то же самое в С# можно сделать.
Select -> map
Aggregate -> fold
аналога reduce в C# нет, но это тот же fold, но без указания начального стейта (т.е. мы сразу агрегируем данные, без нулевого элемента)
3) abList.Tail.Tail вернёт пустой список, поэтому abList.Tail.Tail.Head даст рантайм ошибку — System.InvalidOperationException: The input list was empty.
Да, согласен. Но на F# это даже писать не придётся, компилятор сам выведет :)
Это очень большая тема, но именно она (с примерами на C#) изложена здесь.
Вкратце — в IL нет констрейна на (+), поэтому мы не можем скомпилировать отдельную функцию с таким ограничением. Но можно изхитриться и не компилировать такую "функцию" отдельно, а воспользоваться инлайнингом и разрешать проблемы с типами в контексте CallSite.
Именно это и позволяет делать F#. Эти дополнительные констрейны, которых нет в C#, не магия и не костыль, а всего лишь хитрое использования инлайнинга.
Это из теории категорий. Пример — у вас триллион твитов и вам надо по нажатию кнопочки сформировать срез "среднее кол-во лайков".
Сама по себе сущность tweet — неагрегируема, да и слишком много там ненужной инфы. Поэтому мы создаём дополнительный тип, который содержит нужные нам поля для агрегации и является агрегируемым.
Затем мы преобразуем tweet в новую сущность (map)
И агрегируем новые сущности (reduce)
А чтобы всё это работало в кластере с возможностью разбиения на чанки и инкрементальным добавлением к уже сагрегированым данным новой инфы надо добавить ещё пару ограничений:
1) операция агрегации должна быть ассоциативна — (a+b)+c=a+(b+c)
2) тип для агрегации должен иметь некий Zero элемент: Z+a=a, a+Z=a.
И тогда внезапно выясняется что это моноид!
Списки в F# иммутабл
В ab.Names будут два имени. Но сами значения, которые содержатся в элементах списка заново аллоцироваться не будут.
Пример с комментариями
Конкатенация списков в F# почти бесплатная (ссылку у последнего элемента подменить, причём для этого ему не надо проходить один из списков до конца из-за внутренней оптимизации). Полный аналог в C# — это работа с LinkedList, но для данной цели это тоже неважно, можно и в массивах.
Да, конечно. Он максимально generic.
На C# его сигнатура выглядит так:
Я специально так сделал, чтобы усилить эффект похожести.
int->int->int
let add (x: int) (y: int) = x + y
int * int->int
let add (x: int, y: int) = x + y
Даже с тривиальными примерами Вам пришлось выводить типы явно, руками.
Попробуйте эти 35 строчек на C# изобразить
Картинка
Gist
Есть мнение что на втором же методе случится затык.
на F# можно и так, и вглядываться не надо:
let add (x: int, y: int) = x + y
Только это не нужно, т.к. VS Code над такой функцией напишет int -> int -> int:
let add x y = x + y
Так что это скорее в плюс F#
ошибся веткой
1) Да там смешение по большей части. Инфраструктуру с большим кол-вом мутабельного кода (кеши, стейты, вот это вот всё) проще на C#.
Очень много либ для Akka.Net написано не в функциональном стиле, поэтому для использования в F# какого-нибудь PersistentActor приходится огород городить. Ну а чтобы явно не смешивать два языка для одного фреймворка я просто пишу Akka код на C#, который уже лезет в бизнесовые объекты, сервисы etc в проект, написанный на F#.
2) Бьёте по-больному) Да, для перехода из C# в F# (и наоборот) приходится писать немного мапперов, т.к. Nullable в чистом F# надо заменять на option, например. И не пропускать null внутрь домена вообще.
А в остальном, RecordType из проекта F# в проекте на C# выглядят как класс с конструктором и понятными типами. Если надо хранить его в SQL, то для EF в любом случае надо лепить инфраструктурный класс с описанием индексов и пр. Я не сторонник смешивать доменные модели, DTO и модели для хранения.
Каждый пункт из моего списка выполняется на 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 (дают статическую типизацию к динамическому внешнему контенту) но лучще сразу перейду к основному.
Почему многопоточная молотилка?
Почему доменные модели, ядро?
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?
Почему скрипты?
"Clean"
==> "Build"
==> "Deploy"
Ну и далее, скрипт можно править на лету тем же девопсам или аналитикам не погружаясь в F#.
F# — это инструмент, а не религия. Как инструмент он больше подходит для определённого круга задач.
Для меня этот круг задач выглядит так:
Мотивация в том, что F# умеет решать задачи из круга выше проще, быстрее и лучше.
Почему быстрее и лучше могу написать в отдельном посте.
F# превосходно подходит для описания доменной модели благодаря Discriminated Unions.
Такой код даже выглядит более человекочитаемым для непрограммистов.
На текущем месте работы я придерживаюсь такой схемы:
Инфраструктуру проще на C# потому что:
Писать на F# это не элитизм ни разу. Пишется быстрее и ошибок в логике меньше. Рекомендую попробовать.
Хороший пример DDD на F#.