All streams
Search
Write a publication
Pull to refresh
15
0

User

Send message

Спасибо за результаты. Я правда не знаю о чём это говорит.
Могу только предположить что 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# это даже писать не придётся, компилятор сам выведет :)

  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()?
  1. Не совсем для оптимизации. Особенность компилятора при работе с дженериками. Не стоит на этом заморачиваться, это не главное
  2. Это обычное дело при работе с моноидными типами и агрегацией большого кол-ва данных в разных срезах.
  3. List в F# это действительно список (в C# List — это массив вообще-то), но односвязный.
    Конкатенация списков в F# почти бесплатная (ссылку у последнего элемента подменить, причём для этого ему не надо проходить один из списков до конца из-за внутренней оптимизации). Полный аналог в C# — это работа с LinkedList, но для данной цели это тоже неважно, можно и в массивах.
  4. 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

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


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

Например, 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#

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# — это инструмент, а не религия. Как инструмент он больше подходит для определённого круга задач.
Для меня этот круг задач выглядит так:


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

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

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


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


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

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


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

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


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

Information

Rating
Does not participate
Registered
Activity