Раньше я очень любил C#
Это был мой основной язык программирования, и каждый раз, когда я сравнивал его с другими, я радовался тому, что в свое время случайно выбрал именно его. Python и Javascript сразу проигрывают динамической типизацией (если к джаваскрипту понятие типизации вообще имеет смысл применять), Java уступает дженериками, отстутствием ивентов, value-типов, вытекающей из этого карусели с разделением примитивов и объектов на два лагеря и зеркальными классами-обертками вроде Integer
, отсутствием пропертей и так далее. Одним словом — C# клевый.
Отдельно отмечу, что я сейчас говорю о самом языке и удобстве написания кода на нем.
Тулинг, обилие библиотек и размер сообщества я сейчас в расчет не беру, потому что у каждого
из этих языков они развиты достаточно, чтобы промышленная разработка была комфортной в большинстве случаев.
А потом я из любопытства попробовал F#.
И что в нем такого?
Буду краток, в порядке значимости для меня:
- Иммутабельные типы
- Функциональная парадигма оказалась гораздо более строгой и стройной, чем то, что мы сегодня называем ООП.
- Типы-суммы, они же
Discriminated Unions
или размеченные объединения. - Лаконичность синтаксиса
- Computation Expressions
- SRTP (Статически разрешаемые параметры-типы)
- По умолчанию даже ссылочным типам нельзя присвоить
null
, и компилятор требует инициализацию при объявлении. - Выведение типов или type inference
С null
все понятно, ничто так не засоряет код проекта, как бесконечные проверки возвращаемых значений вроде Task<IEnumerable<Employee>>
. Так что сначала давайте обсудим иммутабельность и одновременно лаконичность.
Допустим, имеем следующий POCO класс:
public class Employee
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public bool HasAccessToSomething { get; set; }
public bool HasAccessToSomethinElse { get; set; }
}
Просто, емко, ничего лишнего. Казалось бы, куда лаконичней?
Соответствующий код на F# выглядит так:
type Employee =
{ Id: Guid
Name: string
Email: string
Phone: string
HasAccessToSomething: bool
HasAccessToSomethingElse: bool }
Вот теперь действительно нет ничего лишнего. Полезная информация содержится в ключевом слове декларации типа данных, имени этого типа, именах полей и их типах данных. В примере из C# в каждой строчке есть ненужные public
и { get; set; }
. Помимо этого, в F# мы получили иммутабельность и защиту от null
.
Ну, положим, иммутабельность мы можем и в C# организовать, а public
с автодополнением написать недолго:
public class Employee
{
public Guid Id { get; }
public string Name { get; }
public string Email { get; }
public string Phone { get; }
public bool HasAccessToSomething { get; }
public bool HasAccessToSomethinElse { get; }
public Employee(Guid id, string name, string email, string phone, bool hasAccessToSmth, bool hasAccessToSmthElse)
{
Id = id;
Name = name;
Email = email;
Phone = phone;
HasAccessToSomething = hasAccessToSmth;
HasAccessToSomethinElse = hasAccessToSmthElse;
}
}
Готово! Правда, количество кода увеличилось в 3 раза: все поля мы продублировали дважды.
Мало того, когда добавится новое поле, мы можем забыть добавить его в параметры конструктора и/или забыть присвоить значение внутри конструктора, и компилятор нам ничего не скажет.
В F# при добавлении поля вам нужно добавить новое поле. Все.
Инициализация же выглядит вот так:
let employee =
{ Id = Guid.NewGuid()
Name = "Peter"
Email = "peter@gmail.com"
Phone = "8(800)555-35-35"
HasAccessToSomething = true
HasAccessToSomethinElse = false}
И если вы забудете одно поле, то код не скомпилируется. Поскольку тип неизменяемый, единственный способ внести изменение — создать новый экземпляр. Но что делать, если мы хотим изменить только одно поле? Все просто:
let employee2 = { employee with Name = "Valera" }
Как это сделать в C#? Ну, вы и без меня знаете.
Добавьте вложенные ссылочные поля, и теперь ваш { get; }
ничего не гарантирует — вы можете изменить поля этого поля. Стоит ли упоминать коллекции?
Но так ли нам нужна эта иммутабельность?
Я не случайно добавил два булевых поля про доступ куда-то. В реальных проектах за доступ отвечает какой-нибудь сервис, и часто он принимает на вход модель и мутирует ее, проставляя где надо true
. И вот я в очередном месте программы получаю такую модель, в которой эти булевы свойства выставлены в false
. Что это значит? Юзер не имеет доступ или просто модель не прогнали еще через аксес сервис? А может прогнали, но там забыли проинициализировать какие-то поля? Я не знаю, я должен проверить и прочитать кучу кода.
Когда же структура неизменяема — я знаю, что там стоят актуальные значения, потому что компилятор обязывает меня полностью инициализировать объект декларации.
В противном случае при добавлении нового поля я должен:
- Проверить все места, где этот объект создается — возможно, там тоже нужно заполнить это поле
- Проверить соответствующие сервисы, мутирующие этот объект
- Написать/обновить юнит-тесты, затрагивающе это поле
Актуализировать маппинги
Кроме того, можно не боятся, что мой объект мутирует внутри чужого кода или в другом потоке.
Но в C# настолько трудно добиться настоящей иммутабельности, что писать такой код просто нерентабельно, иммутабельность такой ценой никак не сэкономит время разработки.
Ну, хватит об иммутабельности. Что еще имеем? В F# мы так же бесплатно получили:
- Structural Equality
- Structural Comparison
Теперь мы можем использовать такие конструкции:
if employee1 = employee2 then
//...
И это действительно будет проверять равенство объектов. Equals
который проверяет равенство по ссылке никому даром не нужен, у нас уже есть Object.ReferenceEquals
, спасибо.
Кто-то может сказать, что это никому не нужно, потому что мы не сравниваем объекты в реальных проектах, поэтому Equals
& GetHashCode
нам нужны так редко, что можно и ручками переопределить. Но я думаю, что причинно-следственная связь тут работает в братную сторону — мы не сравниваем объекты, потому что переопределять руками это все и поддерживать слишком дорого. Но когда это достается бесплатно, применение находится мгновенно: вы можете использовать прямиком ваши модели как ключи в словарях, складывать модели в HashSet<>
& SortedSet<>
, сравнивать объекты не по айдишнику (хотя эта опция, разумеется, доступна), а просто сравнивать.
Discriminated Unions
Думаю, большинство из нас впитали с молоком первого тимлида правило о том, что строить логику на эксепшнах плохо. Например, вместо try { i = Convert.ToInt32("4"); } catch()...
правильней использовать int.TryParse
.
Но помимо этого примитивного и до тошноты затертого примера, мы постоянно нарушаем это правило. Юзер ввел невалидные данные? ValidationException
. Вышли за границы массива? IndexOutOfRangeException
!
В умных книжках пишут, что исключения нужны для исключительных ситуаций, непредсказуемых, когда что-то пошло совсем не так и нет смысла пытаться продолжать работу. Хороший пример — OutOfMemoryException
, StackOverflowException
, AccessViolationException
и т.д. Но вылезти за границы массива — это непредсказуемо? Серьезно? Индексатор на вход принимает Int32
, множество допустимых значений которого составляет 2 в 32 степени. В большинстве случаев мы работаем с массивами, длина которых не превышает 10000. В редких случаях миллион. То есть значений Int32
, которые вызовут исключение сильно больше, чем те, которые отработают корректно, то есть при случайно выбранном инте статистически более вероятно попасть в "исключительную" ситуацию!
То же самое с валидацией — юзер ввел кривые данные. Вот это сюрприз.
Причина, по которой мы активно злоупотребляем исключениями, проста: нам не хватает мощности системы типов, чтобы адекватно описать сценарий "если все нормально, отдай результат, если нет, верни ошибку". Строгая типизация обязывает нас возвращать один и тот же тип во всех ветках исполнения метода (к счастью), но не хватало еще только в каждый тип добавлять string ErrorMessage
& bool IsSuccess
. Поэтому в реалиях C# исключения — пожалуй, меньшее из зол в данной ситуации.
Опять-таки, можно написать класс
public class Result<TResult, TError>
{
public bool IsOk { get; set; }
public TResult Result { get; set; }
public TError Error { get; set; }
}
Но тут нам опять придется написать кучу кода, если мы хотим, например, сделать невалидное состояние невозможным. В примитивной реализации можно присвоить и результат, и ошибку, и забыть проинициализировать IsOk
, так что проблем от этого будет больше, чем пользы.
В F# подобные вещи определяются проще:
type Result<'TResult, 'TError> =
| Ok of 'TResult
| Error of 'TError
type ValidationResult<'TInput> =
| Valid of 'TInput
| Invalid of string list
let validateAndExecute input =
match validate input with // проверяем результат функции валидации
| Valid input -> Ok (execute input) // если валидно - возвращаем "Ок" с результатом
| Invalid of messages -> Error messages // если нет, возвращаем ошибку со списком сообщений
Никаких исключений, все лаконично, и главное, что код самодокументирован. Вам не нужно писать в xml doc, что метод кидает какое-то исключение, вам не нужно судорожно оборачивать вызов чужого метода в try/catch
просто на всякий случай. В такой системе типов исключение — действительно непредсказуемая, неправильная ситуация.
Когда вы кидаете исключения направо и налево, вам нужна нетривиальная обработка ошибок. Вот у вас появляется класс BusinessException
или ApiException
, теперь вам нужно наплодить исключений, отнаследованных от них, следить, чтобы везде использовались именно они, а если вы что-то перепутаете, то вместо, например, 404
или 403
клиент получит 500
. Вас же ждет нудный разбор логов, чтение стек трейсов и так далее.
F# компилятор кидает ворнинг, если мы в match
перебрали не все возможные варианты. Что очень удобно, когда вы добавляете новый кейс в DU. В DU мы определяем воркфлоу, например:
type UserCreationResult =
| UserCreated of id:Guid
| InvalidChars of errorMessage:string
| AgreeToTermsRequired
| EmailRequired
| AlreadyExists
Тут мы сразу видим все возможные сценарии для данной операции, что гораздо наглядней общего списка исключений. А когда мы добавили новый кейс AgreeToTermsRequired
в соответствии с новыми требованиями, компилятор кинул ворнинг там, где мы этот результат обрабатываем.
Я ни разу не видел, чтобы в проектах использовали такое наглядное и описательное множество исключений (по понятным причинам). В итоге сценарии описаны в текстовых сообщениях этих исключений. В такой реализации появляются дубликаты, и, наоборот, разработчики ленятся добавлять новые сообщения, вместо этого делая существующие более общими.
Индексация же по массиву теперь тоже очень лаконична, никаких if/else
и проверки длины:
let doSmth myArray index =
match Array.tryItem index myArray with
| Some elem -> Console.WriteLine(elem)
| None -> ()
Здесь используется тип стандартной библиотеки Option:
type Option<'T> =
| Some of 'T
| None
Каждый раз, когда вы его используете, код сам вам говорит, что отсутствие значения здесь возможно согласно логике, а не из-за ошибки программиста. И компилятор кинет ворнинг, если вы забудете обработать все возможные варианты.
Строгость парадигмы
Чистые функции и expression-based дизайн языка дают нам возможность писать очень стабильный код.
Чистая функция соответствует следующим критериям:
- Единственный результат ее работы — вычисление значения. Она не изменяет ничего во внешнем мире.
- Функция всегда возвращает одно и то же значение для одного и того же аргумента.
Добавьте к этому тотальность (функция может корректно вычислить значение для любого возможного входящего параметра) и вы получите потокобезопасный код, который работает всегда правильно и который легко тестировать.
Expression-based design говорит нам, что все является выражением, у всего есть результат выполнения. Например:
let a = if someCondition then 1 else 2
Компилятор заставит нас учесть все возможные комбинации, мы не можем остановиться просто на if
, забыв про else
.
В C# это обычно выглядит так:
int a = 0;
if(someCondition)
{
a = 1;
}
else
{
a = 2;
}
Здесь легко можно потерять одну ветку в будущем, и a
останется с дефолтным значением, то есть еще одно место, где может сыграть человеческий фактор.
Конечно же, на одних чистых функциях далеко не уедешь — нам нужно I/O, как минимум. Но эти нечистые эффекты можно сильно ограничить до пользовательского ввода и работы с хранилищами данных. Бизнес-логика может быть реализована на чистых функциях, и в этом случае она будет стабильней швейцарских часов.
Уход от привычного ООП
Стандартный кейс: у вас есть сервис, который зависит от парочки других сервисов и репозитория. Те сервисы, в свою очередь, могут зависеть от других сервисов и от своих репозиториев. Все это скручивается могучим DI фреймворком в тугую колбасу функционала, отдается веб-апи контроллеру при реквесте.
Каждая зависимость нашего сервиса, которых в среднем, допустим, от 2 до 5, как и сам наш сервис, обычно имеет 3-5 методов, разумеется, большая часть которых совершенно не нужна в каждом конкретном сценарии. Из всего этого раскидистого дерева методов нам нужно в каждом отдельном сценарии обычно 1-2 метода от каждой (?) зависимости, но мы связываем воедино весь блок функционала и создаем кучу объектов. И моки, конечно же. Куда ж без них — нам нужно же как-то протестировать всю эту красоту. И вот я хочу покрыть тестом метод, но для того, чтобы вызвать этот метод, мне нужен объект этого сервиса. Чтобы его создать, я должен пропихнуть в него моки. Загвоздка в том, чтобы понять, какие именно моки — какие-то в моем методе вообще не вызываются, они мне не нужны. Какие-то вызываются, но только пара методов из них. Поэтому в каждом тесте я делаю нудный сетап этих моков с возвращаемыми значениями и прочей требухой. Потом я хочу протестировать второй сценарий в том же методе. Меня ждет новый сетап. Иной раз в тестах на метод кода больше, чем в самом методе. И да, для каждого метода я должен лезть в его кишки и смотреть, какие же зависимости мне действительно нужны в этот раз.
Проявляется это не только в тестах: когда я хочу использовать какой-то 1 метод сервиса, я должен удовлетворить все зависимости, чтобы создать сам сервис, даже если в моем методе половина из них не используется. Да, это на себя берет DI фреймворк, но все равно все эти зависимости необходимо зарегистрировать в нем. Нередко это может быть проблемой, например, если часть зависимостей лежит в другой сборке, и теперь нам нужно на нее добавить ссылку. В отдельных случаях это может сильно портить архитектуру, и тогда приходится извращаться с наследованием или выделять общий блок в отдельный сервис, тем самым увеличивая число компонентов в системе. Проблемы, безусловно, решаемые, но неприятные.
В функциональной парадигме это работает немного по-другому. Самый крутой пацан здесь — чистая функция, а не объект. И преимущественно, как вы уже поняли, тут используют иммутабельные значения, а не мутабельные переменные. Кроме того, функции прекрасно композируются, поэтому, в большинстве случаев, нам не нужны объекты сервисов вообще. Репозиторий достает из базы то, что тебе нужно? Ну так достань и передай в сервис само значение, а не репозиторий!
Простой сценарий выглядит примерно так:
let getReport queryData =
use connection = getConnection()
queryData
|> DataRepository.get connection // зависимость от коннекшна мы внедряем в функцию, а не в конструктор
// и вот нам уже не нужно следить за lifestyle'ом зависимостей в огромном дереве
|> Report.build
Для тех, кто не знаком с оператором |>
и каррированием, это равносильно следующему коду:
let gerReport queryData =
use connection = getConnection()
Report.build(DataRepository.get connection queryData)
На C#:
public ReportModel GetReport(QueryData queryData)
{
using(var connection = GetConnection())
{
// Report здесь -- статический класс. В него компилируются F# модули
return Report.Build(DataRepository.Get(connection, queryData));
}
}
А поскольку функции прекрасно композируются, можно написать вообще вот так:
let getReport qyertData =
use connection = getConnection()
queryData
|> (DataRepository.get connection >> Report.build)
Заметьте, тестировать Report.build
теперь проще некуда. Вам моки не нужны вообще. Более того, есть фреймворк FsCheck
, который генерирует сотни входных параметров и запускает с ними ваш метод, и показывает данные, на которых метод сломался. Пользы от таких тестов несравнимо больше, они действительно проверяют на прочность вашу систему, юнит-тесты ее скорее неуверенно щекочут.
Все, что вам нужно сделать для запуска таких тестов — 1 раз написать генератор для вашего типа. Чем это лучше написания моков? Генератор универсален, он подходит для всех будущих тестов, и вам не нужно знать имплементацию чего бы то ни было, для того, чтобы его написать.
Кстати, зависимость от сборки с репозиториями или с их интерфейсами теперь не нужна. Все сборки оперируют общими типами и зависят только от нее, а не от друг друга. Если же вдруг вы решите сменить, например, EntityFramework на Dapper, сборку с бизнес логикой это не затронет вообще никак.
Statically Resolved Type Parameters (SRTP)
Тут лучше показать, чем рассказать.
let inline square
(x: ^a when ^a: (static member (*): ^a -> ^a -> ^a)) = x * x
Эта функция будет работать для любого типа, у которого определен оператор умножения с соответствующей сигнатурой. Разумеется, это работает и с обычными статическими методами, не только с операторами. И не только со статическими!
let inline GetBodyAsync x = (^a: (member GetBodyAsync: unit -> ^b) x)
open System.Threading.Tasks
type A() =
member this.GetBodyAsync() = Task.FromResult 1
type B() =
member this.GetBodyAsync() = async { return 2 }
A() |> GetBodyAsync |> fun x -> x.Result // 1
B() |> GetBodyAsync |> Async.RunSynchronously // 2
Нам не нужно определять интерфейс, писать обёртки для чужих классов, имплементить интерфейс, единственное условие — чтобы у типа был метод с подходящей сигнатурой! Я не знаю способа сделать так в C#.
Computation Expressions
Мы рассматривали пример с типом Result
. Допустим, мы хотим выполнить каскад операций, каждая из которых нам возвращает этот самый Result
. И если хоть одно звено в этой цепи вернет ошибку, мы хотим прекратить выполнение и вернуть ошибку сразу.
Вместо того, чтобы писать бесконечные
let res arg =
match doJob arg with
| Error e -> Error e
| Ok r ->
match doJob2 r with
| Error e -> Error e
| Ok r -> ...
Мы можем один раз написать
type ResultBuilder() =
member __.Bind(x, f) =
match x with
| Error e -> Error e
| Ok x -> f x
member __.Return x = Ok x
member __.ReturnFrom x = x
let result = ResultBuilder()
И использовать это так:
let res arg =
result {
let! r = doJob arg
let! r2 = doJob2 r
let! r3 = doJob3 r2
return r3
}
Теперь на каждой строчке с let!
в случае Error e
мы вернем ошибку. Если же все все будет хорошо, в конце вернем Ok r3
.
И вы можете делать такие штуки для чего угодно, включая даже использование кастомных операций с кастомными названиями. Богатый простор для построения DSL.
Кстати, есть такая штука и для асинхронного программирования, даже две — task
& async
. Первый для работы с привычными нам тасками, второй — для работы с Async
. Эта штука из F#, от тасок главным образом отличается тем, что у нее cold start, она также имеет интеграцию с Tasks API. Вы можете строить сложные воркфлоу с каскадным и параллельным исполнением, а запускать их лишь когда они готовы. Выглядит это так:
let myTask =
task {
let! result = doSmthAsync() // суть как у await Task
let! result2 = doSmthElseAsync(result)
return result2
}
let myAsync =
async {
let! result = doAsync()
let! result2 = do2Async(result)
do! do3Async(result2)
return result2
}
let result2 = myAsync |> Async.RunSynchronously
let result2Task = myAsync |> Async.StartAsTask
let result2FromTask = myTask |> Async.AwaitTask
Структура файлов в проекте
Поскольку рекорды (DTO, модели и тд) объявляются лаконично и не содержат никакой логики, в проекте существенно уменьшается количество файлов. Доменные типы могут быть описаны в 1 файле, типы, специфичные для какого-то узкого блока или слоя могут быть определены в другом файле тоже вместе.
Кстати, в F# важен порядок строк кода и файлов — по умолчанию в текущей строчке вы можете использовать только то, что уже описали выше. Это by design, и это очень круто, потому что предохраняет вас от циклических зависимостей. Это так же помогает при ревью — порядок файлов в проекте выдает ошибки проектирования: если в самом верху определен высокоуровневый компонент, значит кто-то накосячил с зависимостями. И это видно с первого взгляда, а теперь представьте, сколько времени вам потребуется для того, чтобы в C# при ревью такое обнаружить.
Для сравнения, вся логика и доменные типы игры Змейка у меня описана в 7 файлах, все кроме одного меньше 130 строк кода.
Итог
Получив все эти мощные инструменты и привыкнув к ним, начинаешь решать задачи быстрее и изящней. Большая часть кода, 1 раз написанная и 1 раз протестированная работает всегда. Писать же снова на C# для меня значит отказаться от них и потерять в продуктивности. Я словно возвращаюсь в прошлый век — вот я бегал в удобных кроссовках, а теперь в лаптях. Лучше, чем ничего, но хуже, чем что-то. Да, в него потихоньку добавляют разные фичи — и pattern matching, и рекорды завезут, и даже nullable reference types.
Но все это, во-первых, сильно позже, чем в F#, во-вторых, беднее. Pattern matching без Discriminated unions & Record destruction — ну, лучше, чем ничего. Nullable reference types — неплохо, но Option
лучше.
Я бы сказал, что главная проблема F# — это то, что тяжело его "продать" сишарпистам.
Но если вы все же решитесь изучить F# — втянуться будет легко.
И тесты будет писать приятно, и от них действительно будет много пользы. Property-based тесты (те, что я описывал в примере с FsCheck) мне несколько раз показали ошибки проектирования, которые силами QA искались бы очень долго. Юнит-тесты же в основном показывали мне, что я забыл что-то обновить в конфигурации тестов. И да, время от времени, показывали, что я что-то где-то упустил в коде. В F# с этим справляется компилятор. Бесплатно.
Комментарии (233)
Yeah
06.11.2018 16:38-1Может кто-то пояснить нубу, как всё вот это ФП применить, ну хотя бы к такой вещи, как построение интерфейса? Вот я понимаю ООП: вот у нас класс кнопки, вот класс окна. Вот у класса окна метод: добавить кнопку, а вот у кнопки метод: нажать. Потому что пример с Report это круто, но тут понятно: имеем поток данных на входе и поток на выходе — посередине функция. Как ФП работает с UI (если работает, конечно)?
urtow
06.11.2018 16:44-1Никак, потому что ФП не про UI, а про бекенд. Как раз таки UI — это ООП. Если у тебя в руках сова — не надо все превращать в глобус
Yeah
06.11.2018 16:47Но на ООП я смогу сделать и отчет, и поток обработать. А на ФП, выходит, не могу? То есть чтобы создать не консольную программку, а что-то посерьезнее, мне нужно два языка?
Szer
06.11.2018 16:48Но на ООП я смогу сделать и отчет, и поток обработать. А на ФП, выходит, не могу? То есть чтобы создать не консольную программку, а что-то посерьезнее, мне нужно два языка?
А много вы промышленных языков знаете, которые умеют ФП и не умеют ООП или наоборот?
a-tk
06.11.2018 16:52-2И в чём тогда преимущества F#, если со львиной долей задач он ничем не поможет?
Szer
06.11.2018 16:55И в чём тогда преимущества F#, если со львиной долей задач он ничем не поможет?
В смысле? Я чот не понял наверное.
F# как раз поможет со всеми задачами, в которые, например, C# умеет.
И многие решает проще.a-tk
06.11.2018 17:00Выше по ветке написано, что
Никак, потому что ФП не про UI, а про бекенд. Как раз таки UI — это ООП. Если у тебя в руках сова — не надо все превращать в глобус
Если мне нужен UI не на веб-стеке, будет ли для меня выгода от использования F# выше чем затраты на его обслуживание?
kagetoki Автор
06.11.2018 17:05+1Во-первых, есть elmish xamarin. Там FP-UI. Я не говорю, что вам нужно его выбрать или что это единственный вариант, но примеры посмотреть уже можно.
Во-вторых, независимо от того, веб у вас или десктоп, в приложении есть бизнес-логика. Ее удобней делать на чистых функциях, как я и написал в статье. Потому что это стабильней и легче тестировать. Сам слой презентации вы можете описать на C#, если вам так больше нравится. Они с F# нормально уживаются даже в рамках одного солюшена.a-tk
06.11.2018 17:11Я не совсем уловил идею: Вы предлагаете делать хост на F#, в который вкладывается C#, или наоборот?
Насколько мне известно, интероп из F# в C# прозрачен, но не каноничен, но система типов F# не ложится легко на C# и взаимодействие весьма болезненное. Или это не соответствует действительности?ForNeVeR
07.11.2018 08:11Сильно зависит от вашего опыта в работе с тем и другим. Я бы вот скорее сказал, что моему опыту это утверждение не соответствует: интероп обычно прозрачен и прост.
Szer
06.11.2018 17:08Если мне нужен UI не на веб-стеке, будет ли для меня выгода от использования F# выше чем затраты на его обслуживание?
Я UI не занимаюсь вообще, поэтому это не ко мне :) Знаю что его на F# делают. Ссылки в каментах.
А если вообще — да, плюсы от использования F# могут перекрыть затраты на поиск разрабов F# / перевоспитание C#-истов. Но не для малого бизнеса имхо.
a-tk
06.11.2018 17:27Очень жаль. Может быть кто-то другой с опытом в это области всё же подскажет.
Про размер команды тоже понятно: с маленькой лучше не рыпаться. Жаль.Vadem
06.11.2018 18:01А я вот не согласен с размером команды.
На мой взгляд, маленькая команда точно так же выиграет от использования ФП.
ForNeVeR
07.11.2018 08:26Вы исходите из предпосылки, что F# — это только про ФП. А между тем там есть и ООП-штуки, и некоторые из них очень интересные — object expressions, например, которых временами не хватает в C#.
0xd34df00d
07.11.2018 22:37Хаскель уже промышленный.
Если вы промышленность определяете как готовность тулчейна и наличие библиотек, конечно же. Распространённость — хреновый критерий. Coq наверняка уже давно сливает Rust'у по критерию распространённости, однако же его вполне можно назвать промышленным.
nsinreal
06.11.2018 18:00Я прошу прощения, но F# в силу своих истоков является мультипарадигменным. Т.е вы можете в рамках F# комбинировать традиционные и новые подходы.
worldbeater
07.11.2018 12:02Конечно, на F# можно разрабатывать не только серверную часть, но и UI. Обязательно посмотрите замечательный доклад от Zaid Ajaj про разработку scalable UI на F#+Elmish, очень познавательно: youtu.be/-Oc4xJivY78 Проще говоря, F# может всё, что могут и C#, и JS
dark_ruby
06.11.2018 16:45+1Ну возьмите хотябы Реакт в качестве примера с единонаправленным потоком данных — UI — это чистая функция зависящая только он входных данных (в данном случае props, state и context) — результатом этой функции является HTML
Yeah
06.11.2018 16:48-1Каждый компонент реакта — это класс как бы
dark_ruby
06.11.2018 16:53Да это класс (но может быть и чистая функция, с пропсами на входе), но классом он только является чтоб иметь доступ к некоторому внутреннему состоянию и метода лайфсайкла. Тем не менее, результатом является вызов render(), хоть пропсу и стейт не подаются в эту функцию явно.
в функциональном программировании вы тоже можете добится внутреннего состояния функции (например с помощью замыканий или монад).
Szer
06.11.2018 16:50Как ФП работает с UI (если работает, конечно)?
Да вот сразу пример (с гифками):
https://github.com/Zaid-Ajaj/tabula-rasa
gleb_kudr
06.11.2018 17:36В некоторых фронтенд фреймворках UI реализован в функциональном стиле.
Там есть иммутабельное хранилище состояния, из которого путем приложения функций с шаблонами прямо выводится текущее состояние интерфейса. Чтобы поменять интерфейс, опять же, чистые функции меняют состояние (точнее, создают новое).
Наличия семантических объектов, к слову, это не отрицает. Просто это не мутабельные объекты, а иммутабельные структуры данных.
Пример: связка React-Redux в мире JS.
Druu
07.11.2018 09:39-1Пример: связка React-Redux в мире JS.
В React-Redux состояние мутабельное и работа с ним ведется при помощи классического мутабельного сценария. Ничего иммутабельного там нет и не пахло даже.
Устал уже это повторять.IvanNochnoy
07.11.2018 13:33-1По-видимому, минусующие эксперты по ФП не видят разницу между immutable и readonly. И почему меня это совсем не удивляет?
0xd34df00d
07.11.2018 22:41Устал уже это повторять.
А объясните для тех, чьё знакомство с вёбом закончилось снежинками за курсором мыши в 2002-м, в чём там драма?
Druu
08.11.2018 07:52Если коротко, есть глобальная переменная state, в которой лежит глобальный стейт приложения, когда ее надо обновить, то делается, условно, state = calculateNewState(state), странные люди по неизвестным мне причинам считают, что у них что-то иммутабельно там, функционально и все такое.
mayorovp
08.11.2018 08:44Ну разумеется, где-то такая переменная должна лежать! Без нее ничего кроме вычислителя формул не сделать на любом языке программирования. Вопрос только в том, в пользовательском коде она находится или в библиотечном. Если в библиотечном — то этот факт никак не мешает пользовательскому коду быть чистым функциональным.
Druu
08.11.2018 08:48Вопрос только в том, в пользовательском коде она находится или в библиотечном.
Вопрос в том, какое апи используется для работы с ней. В редаксе используется обычное процедурное апи, никакого функционального кода там просто нет. Пользователь библиотеки берет и совершенно процедурным образом вызывает процедуру setState(state) которая меняет стейт со вполне процедурной семантикой, еще и колбек может вторым аргументом принимать ("после того как поменяли стейт — сделайте еще вон ту штуку"). Функциональщина изо всех щелей прямл.
mayorovp
08.11.2018 09:02setState используется в React, а не в Redux. В redux используется более функциональный подход (хотя чтобы сделать его совсем чистым, нужны дополнительные телодвижения).
В любом случае, исходно вы говорили про мутабельность — а вот это точно мимо. Что API react, что API redux построены на том, что объект, хранящий текущее состояние, неизменяем.Druu
08.11.2018 11:01+1setState используется в React, а не в Redux.
Да, конечно. Сути не меняет — в Redux у вас будет dispatch(action) — тоже процедура :)
Функциональным подход бы был, если бы dispatch возвращал новый стейт (или новый стор, в котором лежит новый стейт, допустим), и это было бы единственным способом данный новый стейт получить. По факту же диспатч меняет стейт внутри текущего стора.
В любом случае, исходно вы говорили про мутабельность — а вот это точно мимо.
Ну как же мимо? У вас мутабельное хранилище состояния. Вы, конечно, можете попытаться вывернуть все и сказать, что у вас хранилище иммутабельно, просто вы не состояние в нем меняете, а само хранилище меняете :)
Но это будет просто софистика. От того, что вы врапнули один объект в другой иммутабельности не появляется.mayorovp
08.11.2018 11:43Софистика — это то чем занимаетесь вы, а иммутабельность — это свойство структуры данных, а не всей программы в целом.
Druu
08.11.2018 12:04Ну так структура данных, с которой мы работаем (стор), не иммутабельна, в ней есть поле, которое меняет значение (стейт), конец истории.
Вы, конечно, всегда можете взять мутабельную структуру завернуть ее во враппер и потом менять целиком внутри враппера. Появится ли так иммутабельность? Ну конечно нет, это просто перекладывание из пустого в порожнее.
mayorovp
08.11.2018 12:22Пользовательская структура данных — state — иммутабельна. Что внутри у библиотечной — детали реализации.
Druu
08.11.2018 14:03Пользовательская структура данных — state — иммутабельна.
Не-не-не, слушайте. Пользователь работает не со стейтом, он работает со стором. Он на сторе вызывает getState и dispatch, это и есть апи редакса. Это и есть та самая структура, с которой он взаимодействует.
Вот если у вас есть, например, строка, вы берете и делаете к ней запрос подстроки str.substring(n, m) (аналог getState) или замену подстроки str.replace(n, s) (аналог dispatch) — иммутабельность результата substring и аргумента stringReplace не имеет никакого отношения к тому, что сама строка, к которой вы эти методы применяете, мутабельна. Аналогичная история с редаксом.
Именно стор является хранилищем состояния, как объект, предоставляющий апи для запросов и апдейтов. А то, что стор возвращает, когда вы у него требуете текущее состояние — это лишь результат запроса к хранилищу.
Опять же, если вы из бд будете получать иммутабельную коллекцию строк — можно ли сделать вывод, что ваша бд является иммутабельным хранилищем или что состояние, которое в бд хранится — иммутабельно? Нет! Это мутабельное хранилище с процедурным апи.
mayorovp
08.11.2018 14:10-1Вот только строки в рамках общепринятых определений — иммутабельны. Но вы можете продолжать выдумывать свои определения.
Druu
08.11.2018 14:19+1Вот только строки в рамках общепринятых определений — иммутабельны.
В рамках каких общепринятых определений? В сишке null-terminated string с каких пор иммутабельными стали?
Иммутабельным оно будет ровно тогда, когда операции вроде replace будут возвращать вам новую строку, а не модифицировать старую. dispatch же не возвращает новый стор — он модифицирует старый.
Если бы dispatch возвращал новый стор — redux был бы с иммутабельным апи, все верно. Но это не так.
mayorovp
08.11.2018 15:42Строки в javascript, java и c# в рамках общепринятых определений считаются иммутабельными. Несмотря на то, что чаще всего хранятся (о ужас!) в изменяемых переменных или объектах.
Druu
08.11.2018 16:02Строки в javascript, java и c# в рамках общепринятых определений считаются иммутабельными.
Да ради бога, но, еще раз, они иммутабельны потому, что когда вы выполняете операцию вида replace или похожую, то вам возвращается новая строка. А старая остается как была. Если же replace меняет текущую строку — то строки у вас мутабельные.
Так вот, dispatch в редаксе меняет текущий стор, не создает нового.
TheShock
08.11.2018 14:57+1Вот только строки в рамках общепринятых определений — иммутабельны. Но вы можете продолжать выдумывать свои определения.
И что? Иммутабельные строки можно изменять процедурно? Ну вот есть функция
dispatch(targetString)
Как функция должна быть написана, чтобы строка поменялась?mayorovp
08.11.2018 15:57Нет, иммутабельные строки нельзя изменять процедурно. Точно так же как нельзя процедурно изменить иммутабельное состояние, только заменить его новым.
Druu
08.11.2018 16:03Точно так же как нельзя процедурно изменить иммутабельное состояние
Но в редаксе я через диспатч меняю состояние. Вывод? Оно мутабельно.
только заменить его новым.
скажите, вы когда в бд запись обновляете — это изменение имеющегося состояния или создание нового? Как отличить два этих варианта по поведению?
mayorovp
08.11.2018 16:05Нет, вы через дистатч заменяете состояние новым. Старый объект состояния остается в неизменном виде.
Druu
08.11.2018 16:11Нет, вы через дистатч заменяете состояние новым. Старый объект состояния остается в неизменном виде.
Какой старый объект? С чего вы взяли, что он вообще есть? Редакс вам не дает никаких гарантий по поводу того, как именно хранится состояние. Он дает гарантии лишь по поводу того, что вернет getState. Возможно, ваш объект формируется ровно в тот момент, когда вызван getState, а до этого ег ов принципе не существует. Конечно же он каждый раз будет новый. Точно так же как каждый раз у вас будет новой строкой результат ф-и substring. Но строки при этом как были мутабельные так ими и остаются, если replace работает мутабельно. Важно поведение replace, а не поведение substring, понимаете?
Повторяю вопрос про бд — вы когда там данные обновляете, то вы их заменяете на новые или модифицируете существующие?
TheShock
08.11.2018 18:08Но строки при этом как были мутабельные так ими и остаются, если replace работает мутабельно.
Я бы в качестве примера привел массивы в JS. Да, у них есть куча иммутабельных методов. slice(), map(), но ведь есть и мутабельные, такие как sort() или splice(). И если мы пользуемся только иммутабельными, то да, мы массивами пользуемся словно иммутабельными. Но как только мы начинаем менять элементы в самом массиве — он перестает считаться иммутабельным, даже если до этого мы 10 раз воспользовались иммутабельными методами.
const array = oldArray .map(/* ... */) .filter(/* ... */) .reduce(/* ... */); // Да, пока мы им пользуемся в ФП стиле array[5] = null; // Плевать, как мы раньше пользовались, теперь это обычная процедурщина, пускай и с элементами ФП.
Девушке вполне достаточно один раз переспать с мужчиной, чтобы перестать быть девственницой, даже если до 16-ти она ни разу ни с кем не целовалась.
mayorovp
08.11.2018 18:54Э-э-э, нет. Редакс именно что дает гарантии что это будет новый объект, а старый останется на месте (при условии соблюдения контракта всеми редьюсерами).
Druu
08.11.2018 19:21-1Э-э-э, нет. Редакс именно что дает гарантии что это будет новый объект, а старый останется на месте (при условии соблюдения контракта всеми редьюсерами).
А я что сказал? Редакс вам дает гарантии того, что возвращается из getState, точно так же, как в случае строк, даются гарантии того, что возвращается из substring. Только это все не имеет отношения к иммутабельности. К иммутабельности имеет отношение то, как работает dispatch (aka replace). В случае иммутабельных строк replace возвращает новую строку, оставая неизменной старую, а в случае dispatch — вам возвращается что угодно в зависимости от упоротости миддлеваре и при этом старый стор оказывается измененным (в частности, если на старом сторе вызвать getState, то он вернет не то, что раньше).
TheShock
08.11.2018 21:28Редакс вам дает гарантии того, что возвращается из getState
Никаких гарантий Редакс не дает, это ведь процедурный фреймворк. Вот пример. Открываем доку thunk, берем их пример и смотрим, что возвращает getState().
function incrementAsync() { return (dispatch, getState) => { const state1 = getState(); setTimeout(() => { const state2 = getState(); dispatch(increment()); }, 1000); }; }
getState вроде как один, вроде как чистая функция, а возвращает совершенно разные стейты, если они поменялись за эту секунду.mayorovp
08.11.2018 21:39Не вижу каким образом ваш пример демонстрирует отсутствие иммутабельности state1 или state2.
TheShock
08.11.2018 22:20Ну, во-первых, state1 и state2 мутабельны по всей глубине в силу того, что JS — не ФП-язык.
Во-вторых, это пример процедурности — грязно мутируется глобальная переменная. И процедура getState() — не чистая функция.
Так зачем этот процедурный фреймворк называть функциональным?
Druu
09.11.2018 09:02Не вижу каким образом ваш пример демонстрирует отсутствие иммутабельности state1 или state2.
При чем тут state1/state2 если мы обсуждаем стор?
mayorovp
09.11.2018 09:41А зачем вы обсуждаете стор?
Druu
09.11.2018 11:18Потому что стор является хранилищем состояния в редаксе.
Напомню про исходный тезис:
В некоторых фронтенд фреймворках UI реализован в функциональном стиле.
Там (в Redux) есть иммутабельное хранилище состояния, из которого путем приложения функций с шаблонами прямо выводится текущее состояние интерфейса.Это неверно, ничего иммутабельного в редаксе нет. Ничего функционального нет тоже.
TheShock
08.11.2018 18:02Нет, иммутабельные строки нельзя изменять процедурно. Точно так же как нельзя процедурно изменить иммутабельное состояние, только заменить его новым.
Стоп. Но я ведь вызываю процедуру dispatch и состояние меняется.
Ну вот, допустим, хочу покрыть тестами. Как это выглядит в ФП:
const newState = dispatch(data, oldState); // старый стейт остался старым checkIsOld(oldState); // новый стейт стал новым checkIsNew(newState);
Как это делается в процедурном редаксе:
// мне тут даже не надо передавать стейт, это грязная процедура! dispatch(data); // опа, поменялась глобальная переменная! checkIsOld(globalState);
То есть одна из немногих функций редакса, его core, который мы постоянно используем в клиентском коде:
1. Зависит не только от аргументов
2. Имеет серьезные побочные эффекты
3. Является недетерминированно
Теперь я открываю википедию, статью «Функциональное программирование» и читаю:
понятия «функции» в императивном программировании заключается в том, что императивные функции могут опираться не только на аргументы, но и на состояние внешних по отношению к функции переменных, а также иметь побочные эффекты и менять состояние внешних переменных. Таким образом, в императивном программировании при вызове одной и той же функции с одинаковыми параметрами, но на разных этапах выполнения алгоритма, можно получить разные данные на выходе из-за влияния на функцию состояния переменных. А в функциональном языке при вызове функции с одними и теми же аргументами мы всегда получим одинаковый результат: выходные данные зависят только от входных
И, если имеем хоть каплю критического мышления, чтобы снять с ушей маркетологическую лапшу, то понимаем, что Редакс — обычная процедурщина, которая косит под ФП, но противоречит его основным принципам.mayorovp
08.11.2018 18:55Ну, модульные тесты как бы должны тестировать ваш код (редьюсеры), а не библиотечный (стор).
Но даже если вы при тестировании редьюсера используете стор — ничего не мешает старый объект сохранить:
const oldState = store.getState(); dispatch(data); const newState = store.getState(); // старый стейт остался старым checkIsOld(oldState); // новый стейт стал новым checkIsNew(newState);
Druu
08.11.2018 19:24Но даже если вы при тестировании редьюсера
А при чем ту тестирование редьюсера, если речь о тестировании кода, который работает с хранилищем?
Но даже если вы при тестировании редьюсера используете стор — ничего не мешает старый объект сохранить:
Вы не тот объект сохранили, надо:
const oldStore = store; store.dispatch(data); const newStore = store; checkIsOld(oldStore.getState()); checkIsNew(newStore.getState());
mayorovp
08.11.2018 20:05А при чем тут тестирование кода, который работает с хранилищем, когда я говорю о работе с состоянием?
Вы не тот объект сохранили
Нет, это вы не тот объект сохранили. Потому что иммутабельным является состояние, а не стор.
Druu
09.11.2018 09:05А при чем тут тестирование кода, который работает с хранилищем, когда я говорю о работе с состоянием?
Каким состоянием? У вас есть хранилище и в нем хранятся данные. Вы эти данные можете запрашивать через getState(). getState() вам возвращает некоторую структуру. Опустим тот факт, что данная структура мутабельна, и зададимся вопросом — при чем тут вообще ее свойства?
Еще раз, вы делаете запрос к бд, вам возвращается иммутабельная коллекция иммутабельных строк. Ваше состояние в бд иммутабельно? Да или нет?
TheShock
08.11.2018 19:32Но даже если вы при тестировании редьюсера используете стор — ничего не мешает старый объект сохранить:
Я понимаю как тестировать процедурный код, спасибо.
TheShock
08.11.2018 21:11Та и не в тестах же ж дело. Я просто на примере доказал процедурность редакса.
mayorovp
08.11.2018 21:40Но обсуждается-то тут не процедурность редакса, а иммутабельность состояния.
TheShock
08.11.2018 21:58Тут обсуждается процедурность редакса в первую очередь. И мутабельное изменение глобальной переменной как одно из проявлений этой процедурности
mayorovp
08.11.2018 22:11Не знаю что обсуждаете вы, но я спорю вот с этим:
В React-Redux состояние мутабельное
TheShock
08.11.2018 22:28Ну так состояние мутабельное. Да, объекты в нем принято не мутировать, но само состояние глобально изменяется.
vintage
09.11.2018 09:43-1let store = Redux.createStore( ()=> [1,2,3] ) someWildFunc( store.getState() ) console.log( store.getState() ) // [3,2,1] function someWildFunc(a) { return a.reverse() }
Состояние вы, конечно, можете не менять, но именно редакс вам никаких гарантий не даёт. Более того, он даже не узнает о том, что состояние где-то поменялось, что приведёт, странного вида глюкам, когда состояние в сторе одно, а реактивно зависимая от него вьюшка показывает другое. И сиди, ищи концы, где кто забыл вызвать slice перед reverse.
Иммутабельность — оно про невозможность изменений, а не про только лишь их отсутствие в конкретном месте в конкретное время.
mayorovp
09.11.2018 09:54Приведенный вами код не является корректным кодом с точки зрения redux.
vintage
09.11.2018 10:55Обоснуйте. Впрочем, вы сейчас подменяете тезис «состояние в редакс иммутабельное» на «состояние полученное из редакс нельзя без глубокого копирования передавать туда, где оно может быть изменено, иначе бо-бо».
Druu
09.11.2018 11:21-1«состояние в редакс иммутабельное»
Мне непонятно, зачем вообще обсуждать, мутабелен или нет объект, который возвращается из getState()? Какое это отношение к делу имеет? Допустим, он мутабелен, но при этом getState() бы возвращал глубокую копию внутреннего _state. Что-то вообще бы изменилось, кроме перформанса и принципиальной невозможности сломать этот внутренний стейт, в отличии от имеющейся ситуации?
0xd34df00d
09.11.2018 07:00Зашёл оставить это комментарий после прочтения предыдущего вашего.
Если вы в нёдра рантайма отдаёте функцию State -> State, то всё чисто, ага. Если вы руками делает присваивание, то ничего функционального. Если, более того, в языке захардкожены правила вычисления только для этого присваивания как частный случай, то это совсем ппц.Druu
09.11.2018 09:12Если вы в нёдра рантайма отдаёте функцию State -> State, то всё чисто, ага.
Ну нет, если functionalSetState(f) = setState(f(getState())) то ничего чисто тут не будет. Вопрос в том, можем ли мы заметить то, что где-то что-то было мутировано (понятное дело в js нету способов это надежно обеспечить, по-этому речь о том что мы это не заметим, если сами себя ограничиваем и используем библиотеку идиоматическим способом).
0xd34df00d
09.11.2018 17:51Так если ваш язык чистый, и вы отдаёте функцию с упомянутой сигнатурой, то заметить, что что-то там мутируется, вы не сможете.
Ну как в хаскеле, вы отдаёте набор IO-экшонов, которые абсолютно чистые (потому что это _описание_), а рантайм чё-то там потом мутирует и всё такое.Druu
09.11.2018 22:48Так если ваш язык чистый, и вы отдаёте функцию с упомянутой сигнатурой, то заметить, что что-то там мутируется, вы не сможете.
В данном случае мы можем, т.к. у нас есть точка входа, которая предоставляет апи (store), у которой мы просим состояние (getState()) и оно разное до и после store.dispatch()
Ну как в хаскеле, вы отдаёте набор IO-экшонов, которые абсолютно чистые (потому что это описание), а рантайм чё-то там потом мутирует и всё такое.
В хаскеле мы, формально, в ф-ю засовываем World и возвращаем новый World, а монадический интерфейс гарантирует, что старый World потрогать не удастся. По-этому мы и не можем выяснить, изменился старый World или нет. Тут у нас ситуация такая:
const oldState = store.getState() store.dispatch(f) //передаем в стор чистую ф-ю const newState = store.getState() // oldState != newState, newState = f(oldState)
в принципе такой диспатч ничем не отличается от:
const oldState = store._state store._state = f(oldState) const newState = store._state // oldState != newState, newState = f(oldState)
с-но диспатч внутри что и делает — применяет f к стейту и заменяет новым старый
TheShock
07.11.2018 14:27-1Пример: связка React-Redux в мире JS.
Меня так забавляет, когда люди процедурный фреймворк приводят в пример в качестве функционального.
Vadem
06.11.2018 17:57Лично для меня писать в функциональном стиле UI гораздо проще и приятнее чем в ООП стиле.
В комментариях советуют Elmish, но я бы порекомендовал взглянуть на Elm.
У него введение лучше. Возможно из него станет понятно как писать UI в функциональном стиле — An Introduction to Elm.vyatsek
08.11.2018 15:49А как будет выглядеть стандартная реализация иерархии контролов, пусть даже древних финформз.
Или вы говорите о реализации бизнес логики в функциональном стиле?
svk28
07.11.2018 00:09Хмм, а что там сложного? Я может что ни так понял, но есть библиотека реализующая гуй, у нее есть api. В своем коде дергаем этот апи и все. Если абстрагироваться от F#, то один из самых ярких примеров это ТК, где gui пишется в функциональном стиле легко и просто.
chirkin
07.11.2018 08:46Для UI есть Functional Reactive Programming (FRP).
Вот здесь, например, весь UI и весь WebGL код написан на Haskell (скомпилирован в JS через ghcjs).
UPD: прошу прощения, не заметил что продублировал ответ других пользователей.
cadovvl
07.11.2018 14:17+2Когда-то лет 7 назад слушал доклад ребят, которые на каком-то диалекте LISP web- разработкой занимались и были очень довольны. Такчто, думаю, возможно все…
kmk
07.11.2018 15:14Есть два стула...: Структуры и Функции. Например, структуры: Window, Button, Theme — это просто данные, анемичная модель. Функции(глобальные методы): AddChildToParent(button, window), ApplyThemeTo(button, theme), AdjustLayout(window), Show(window) и т.п.
time2rfc
06.11.2018 17:07+1Спасибо за еще одну статью о ФП на F#, последнее время они все чаще начали меня радовать(субьективно кажеться, что раньше реже такие материалы писали).
Интересно было бы посмотреть на тесты производительности функционального и ооп подхода на .net платформе.
VanKrock
06.11.2018 17:10Буквально сегодня добавил в проект расширение:
public static class Extensions { public static T With<T>(this T @object, Action<T> action) { action?.Invoke(@object); return @object; } }
Думаю, как же удобно менять свойства объекта или вызывать несколько методов подряд, некий аналог builder паттерна. Оказалось сделал как в F#.
GrigoryPerepechko
07.11.2018 12:22+1Паттерны тут не при чем. Это method chaining
А зачем вы делаете вызов через?..VanKrock
07.11.2018 20:16Потому что action может быть null. Тут есть вариант, что я должен выбросить ArgumentNullException, но я так не делаю, потому-что считаю, раз у меня уже есть объект, а вместо метода, который должен его модифицировать передали null, то этот объект модифицировать не нужно. То есть:
— Модифицировать как?
— Никак
Не модифицирую.
klvov
06.11.2018 17:15Автор вот пишет:
но не хватало еще только в каждый тип добавлять string ErrorMessage & bool IsSuccess.
Тем не менее, это вполне работающая практика. В golang так делают, в некоторых проектах в nodejs тоже принят такой стиль обработки ошибок. И этот подход можно использовать почти везде, даже в языках, где ООП нет вообще. Не самый удобный способ (напоминающий реализацию монады Maybe вручную), но работающий, простой, понятный и приводящий обработку ошибок к единообразному стилю.Szer
06.11.2018 17:19+1Тем не менее, это вполне работающая практика. В golang так делают, в некоторых проектах в nodejs тоже принят такой стиль обработки ошибок. И этот подход можно использовать почти везде, даже в языках, где ООП нет вообще.
В Go не от хорошей жизни так. Как только в языке появляются человеческие типы-суммы об этом ужасе надо забывать. Зачем брать в пример неудачные реализации?
creker
06.11.2018 23:11Где так в golang делают? Либо я чего-то не понял, либо речь о чем-то другом. Возврат нескольких значений в функциях дает почти то самое optional — либо результат, либо ошибка. И не нужные никакие извращения со строчками и булевыми переменными.
klvov
07.11.2018 21:31Где так в golang делают?
Ну вот на Техтрэйне Алексей Акулович из ВК рассказывал, что эту идиому в Go часто используют:
0xd34df00d
07.11.2018 22:44+2Возврат нескольких значений в функциях дает почти то самое optional — либо результат, либо ошибка.
Только опшонал — это монада. Можно писать в монадическом стиле (как в статье в конце, видимо, но я F# плохо читаю). В golang можно писать в монадическом стиле?
AnarchyMob
06.11.2018 17:26Immutability, Structural Equality, Structural Comparison…
Скоро завезут в C# records (с выходом восьмеркиили десятки)
Документация
ОбсуждениеSzer
06.11.2018 17:29Скоро завезут в C# records (с выходом восьмерки)
Бабка на двое сказала. Только пропозал есть с мая. До сих пор в том же состоянии.
a-tk
06.11.2018 17:33Хотелось бы поднять один не технический, но методологический вопрос.
Вот выходит на хабре очередная статья по некоторой технологии, которая в какой-то мере считается элитной в силу своей нераспространённости или сложности.
Приходит некоторое количество новичков, которые вроде как готовы в это ввязаться и задают вопросы о том, чем указанная технология может быть полезна им для решения их повседневных задач. И получают ответы, что технология вообще не для них, не для их задач, а для обучения плаванью сферических слонов в вакууме, притом всё это обильно посыпается минусами спрашивающих (видимо, с мыслью «ну тупыыыыые!»). Какие выводы он сделает? Логичные: да нафиг ему нужна непонятная технология с токсичным сообществом? А ведь он был почти готов присоединиться.
Доколе? Доколе сообщества адептов «элитарных» технологий, к коим функциональщики себя бесспорно относят, будет публикация статей для почёсывания эга и пинание раздумывающих новичков?AnutaU
06.11.2018 17:39У F# на редкость дружелюбное сообщество. В соответствующем телеграм чате новичкам всегда рады и на вопросы постоянно отвечают. И здесь вам тоже ответили, нужные ссылки привели, уточнить если что всегда можно. По-моему, здесь не в сообществе проблема, а вам очень хочется придраться.
a-tk
06.11.2018 17:43Про телеграм чат я знаю и его почитываю, к нему претензий совсем нет. Однако см. выше есть тема про UI. Первый же вопрос — первый же минус (я не про себя)
nsinreal
06.11.2018 17:57+1Однако несколько людей в том вопросе начала отвечать нейтрально с точки зрения дружелюбия. Не стоит судить сообщество по отдельным товарищам.
Впрочем, сам вопрос "как применять ФП" ужасно большой, сравним с "как применять ООП" или "как программировать". На него трудно дать ответ в комментарии. Это притягивает тех, кто ответ на собирается давать.
White_Scorpion
07.11.2018 11:55-1Нейтральные ответы — это конечно хорошо, но кроме нейтральных ответов — человеку прилетел и вполне себе конкретный "минус", который, скажем так,"энтузиазм несколько поубавил". А ну как прийдёт кто и ещё до кучи карму гробанёт? Сидишь и думаешь: нуево нафиг вопросы задавать. А нет вопросов — нет и аплогетов.
a-tk
06.11.2018 17:37В C# это обычно выглядит так:
int a = 0; if(someCondition) { a = 1; } else { a = 2; }
Вообще-то в C# есть (1) тернарный оператор, (2) контроль ветвей, не инициализирующих данные. Если в первой строке не делать инициализации, то компилятор код без else не пропустит. Если, конечно, данные эти кому-нибудь нужны.Szer
06.11.2018 17:50Вообще-то в C# есть (1) тернарный оператор
Ну такое. Это не компилируется:
var flag = true; var _ = flag? Console.WriteLine("true") : Console.WriteLine("false");
Такое почему-то тоже
var a = 0; var b = flag ? { Console.WriteLine("true"); a = 1; } : { Console.WriteLine("false"); a = 2; };
Так что тернарный оператор в C# конечно уступает expression-based if из F#
a-tk
06.11.2018 17:53А это тут при чём? О_о
Если сильно хочется, тоConsole.WriteLine(flag ? "true" : "false")
Во втором примере вообще желание чего-то странного, я даже толком мысли не понял.Szer
06.11.2018 17:59+1Во втором примере вообще желание чего-то странного, я даже толком мысли не понял.
Это пример того что C# это всё же язык про стейтменты. Нельзя куда угодно засунуть какой хочешь экспрешн.
В if-стейтменте можно делать что хочешь (явные блоки { }), но это не выражение, оно ничего не возвращает (поэтому проверки типов у разных веток if нет и быть не может).
В ?-операторе ничего делать нельзя (явные блоки { } не поставишь), но оно обязано возвращать, а т.к. C# не умеет в unit/Void, то писать в ?-операторе unit/Void операции нельзя.
a-tk
06.11.2018 18:51Странно требовать от языка того, чего нет в его спецификации.
Szer
06.11.2018 20:03+1Все пропозалы — это требования того, чего нет в спецификации. Если этого не требовать, язык перестанет развиваться.
Все кто пишут пропозалы — странные?
a-tk
06.11.2018 21:06+1А давайте из языка уберём слово return и получим какой-нибудь Rust. А ещё можно вспомнить Pascal с его переменной Result, которой в любой момент можно присвоить значение, и оно будет возвращаемым из функции.
Любой язык имеет свою семантику и странно требовать забить на всё и подстроить его семантику под хотелки приверженцев других семантических оборотов.roscomtheend
07.11.2018 09:43Сколь помню, это не классический паскаль, в классическом надо присваивать имени функции.
vdem
06.11.2018 17:44type Employee = { Id: Guid Name: string Email: string HasAccessToSomething: bool HasAccessToSomethingElse: bool }
Вот теперь действительно нет ничего лишнего.
Двоеточия и знак равенства лишние, можно ведь усугубить (скажем, в каком-нибудь F##)
type Employee { Guid Id string Name string Email bool HasAccessToSomething bool HasAccessToSomethingElse }
Вот теперь действительно нет ничего лишнего, но думаю и дальше можно урезать.
Employee G Id s Name s Email b HasAccessToSomething b HasAccessToSomethingElse
На правах шутки юмора :)Szer
06.11.2018 17:52Добавим чутка скобочек и вот он LISP!
vdem
06.11.2018 22:03Зачэм скобочки, родной? Прабэлы как в пэтоне, ага.
FoggyFinder
07.11.2018 14:25+1С сокращениями названий для типов F# может помочь :-)
type s = string type b = bool
a-tk
06.11.2018 17:49+1Можно ещё один момент по приведённым примерам пояснить?
Вот есть у нас тип и значение:
type Employee = { Id: Guid Name: string Email: string HasAccessToSomething: bool HasAccessToSomethingElse: bool } let employee = { Id = Guid.NewGuid() Name = "Peter" Email = "peter@gmail.com" Phone = "8(800)555-35-35" HasAccessToSomething = true HasAccessToSomethinElse = false}
В каком месте здесь происходит сопоставление сущности с типом? Только вывод типа компилятором? Тогда почему этот код не может поломаться при неумелом рефакторинге?Szer
06.11.2018 17:57Здесь компилятор F# идёт вверх по коду и ищет первый подходящий объявленный record (в нём обязаны совпадать все поля). И неявно выводит тип
Employee
у идентификатораemployee
.
Оно может поломаться если между объявлением
type Employee =
и созданием
let employee = ...
объявить другой рекорд с ровно теми же полями. Тогда компилятор неявно выведет этот самый другой тип у этой же переменной.
Это конечно поломает код (строгая типизация жеж) и вы сразу заметите ошибку в IDE, так что проблем нет. Если такой шадоуинг типов не ломает билд, то значит предыдщий рекорд ничего полезного не делал :)
a-tk
06.11.2018 18:53А если будет два типа, отличающиеся на одно поле, или два типа с одинаковым набором полей?
type Celsius = { Value : float } type Farenheit = { Value : float } let v = {Value = 36.6 }
Что тут будет?AnutaU
06.11.2018 19:03Что тут будет?
Здесь выведется последний объявленный тип. Неоднозначность можно явно разрешить при необходимости.
Для конкретно вашего случая (чтобы не путать единицы измерения) очень удобно использовать single case discriminated union:
type CustomerId = CustomerId of int // define a union type type OrderId = OrderId of int // define another union type
(Пример взяла отсюда)AnutaU
06.11.2018 23:42+1Хм, для единиц измерения есть специальная штука. Упустила слегка.
0xd34df00d
07.11.2018 23:04Ну это же должно быть в библиотеках, а не в ядре языка! Например, если упороться.
a-tk
08.11.2018 11:41(Минутка оффтопа)
Интересно, а в математических пакетах типа Матлаба такое есть?
0xd34df00d
07.11.2018 22:51А зачем так сделали? Чтобы сэкономить на аннотациях типов для подобных топ-левел-байндингов?
worldmind
06.11.2018 18:24-1Python начиная с 3.5 позволяет использовать статическую типизацию, хотя конечно питон не хаскел.
Arseny_Info
07.11.2018 13:17это не полноценная типизация, это аннотации, которые сейчас никак не используются интерпретатором
worldmind
08.11.2018 11:35И? Что мешает запускать mypy? Наоборот это лучше чем «полноценная типизация» т.к. есть выбор, каждый решает сам нужна ли для его проекта статическая типизация.
0xd34df00d
07.11.2018 23:04А эту статическую типизацию можно верифицировать, собственно, статически, и нет ли тут где-нибудь внезапного решения проблемы останова?
impwx
06.11.2018 18:24+4Хотя я очень люблю фшарп и ценю усилия по его популяризации, в этой статье есть одна серьезная проблема — фшарп преподносится как серебряная пуля, а в сравнении с конкурентами используется грязная риторика, которая напрочь подрывает все доверие к материалу:
Добавьте вложенные ссылочные поля, и теперь ваш { get; } ничего не гарантирует — вы можете изменить поля этого поля.
Фшарп тоже не гарантирует иммутабельность вложенных объектов, как и любой другой CLR-совместимый язык:
type Test = { Values: int[] } let x = { Values = [| 1; 2; 3 |] } x.Values.[0] <- 5
при случайно выбранном инте статистически более вероятно попасть в «исключительную» ситуацию
Откуда тут вообще взялся случайно выбранный int? Это наглое жонглирование статистикой в пользу вашего утверждения.
В C# это обычно выглядит так:
И явно тип переменной указали, и скобки расставили, и про тернарный оператор условия тактично умолчали. Только обычно такое записывают как
int a = 0; if(someCondition) { a = 1; } else { a = 2; }
var a = someCondition ? 1 : 2
.
Ну и так далее в этом духе. Потенциальных пользователей такие толстые приемы скорее оттолкнут.kagetoki Автор
07.11.2018 05:16Видимо, я неточно выразился.
Во-первых, F# точно не является серебрянной пулей: система типов в хаскеле, например, мощнее, а в C# естьnameof
, partial classes, которые делают его более удобным для генерации code behind. Я, например, не знаю как F# работает с WPF — не пробовал.
Во-вторых, я использовал упрощенные примеры, просто потому что так проще писать статью.
Да, в F# можно объявить обыкновенный мутабельный класс, и тогда компилятор не защитит это поле в рекорде или в DU. Мой поинт не в том, что F# покроет все магической защитой, а в том, что F# позволяет легко создавать и работать с неизменяемыми структурами, в отличие от C#. Да, массив все еще изменяемый, но из коробки в F# есть неизменяемыеList
,Set
&Map
.
По поводу случайного инта — я, конечно, ни в коем случае не ожидаю, что кто-то будет случайным образом пихать аргументы в индексатор, в конце концов, проверка длины массива — одно из первых правил, которое выучивает юный программист. Я лишь добавил это как демонстрацию того, насколько функция далека от тотальности. Тем не менее, несмотря на то, что мы научнены жизнью и делать так не будем, факт остается фактом: функция написана так, что бОльшая часть диапазона входных параметров вызовет исключение.
Что касается примера с
if/else
, я знаю про тернарный оператор, да. И проvar
, я без всякой задней мысли поставил тамint
, ошибся, сорян. Опять-таки, пример упрощен, и я там говорю про добавление веток в будущем. Можно сделать вложенные тернарные операторы, но я так делать не люблю из-за плохой читаемости.
Давайте сделаем пример более боевым:
let myResult = if condition then let a = myFunc arg1 arg2 let b = myFunc2 arg3 a + b elif condition2 then myFunc arg4 arg5 else myFunc2 arg3
При большом желании можно это запихать в тернарные операторы, но обычно так не делают и теряют эту проверку ветвей.
mayorovp
07.11.2018 08:02+1В такиз случаях заводится неинициализированная переменная myResult. При чтении из нее компилятор проверит что значение ей было присвоено во всех ветвях исполнения.
Druu
07.11.2018 13:49Можно еще по идее сделать Id монадку и записывать в аналогичном виде через LINQ, если сахар для where подойдет
ЗЫ: хотя не, не получится
FoggyFinder
07.11.2018 14:30+1Я, например, не знаю как F# работает с WPF — не пробовал.
Работает прекрасно
Знакомство с Gjallarhorn.Bindable.WPF (F#) на примере выполнения тестового задания
TheShock
07.11.2018 14:33+1Давайте сделаем пример более боевым:
let myResult = if condition then let a = myFunc arg1 arg2 let b = myFunc2 arg3 a + b elif condition2 then myFunc arg4 arg5 else myFunc2 arg3
И он сразу становится кандидатом на рефакторинг даже в F#
codecity
06.11.2018 21:58Соответствующий код на F# выглядит так:
type Employee = { Id: Guid Name: string Email: string HasAccessToSomething: bool HasAccessToSomethingElse: bool }
А если захотите добавить проверку, к примеру что Email не null и не пустая строка?kagetoki Автор
07.11.2018 05:21Разные способы есть. Если email по бизнес логике обязателен, то, например, можно вернуть ошибку с помощью DU еще до создания этого экземпляра, но мне нравится вот такой подход:
module Email = type EmailAddress = private | ValidEmail of string | InvalidEmail of string let ofString = function | "validEmail" -> ValidEmail "validEmail" | invalid -> InvalidEmail invalid let (|ValidEmail|InvalidEmail|) = function | ValidEmail email -> ValidEmail email | InvalidEmail email -> InvalidEmail email open Email let invalid = Email.ofString "invalid" let valid = Email.ofString "validEmail" match invalid with | InvalidEmail invalid -> printfn "invalid was InvalidEmail %s" invalid | ValidEmail valid -> printfn "invalid was ValidEmail %s" valid match valid with | InvalidEmail invalid -> printfn "valid was InvalidEmail %s" invalid | ValidEmail valid -> printfn "valid was ValidEmail %s" valid
Тут мы убрали возможность создавать мыльник напрямую, но оставили возможность матчить валиден ли он или нет.
codecity
07.11.2018 06:03Ну вот тут, видимо, проблема и раскрывается. В C# свойства то и нужны для простой возможности добавить проверку/логирование etc., а иначе можно было бы использовать те-же readonly-поля.
kagetoki Автор
07.11.2018 06:16Вы добавляете логирование в свойства ДТО/моделей? А что вы предлагаете делать, когда мыльник пустой пытаются присвоить?
worldbeater
07.11.2018 12:12+2Главное преимущество и цель системы типов F# в том, чтобы не допустить существование структур, содержащих некорректные значения. Making invalid states unpresentable, как пишет Scott Wlaschin в своей книге «Domain Modelling Made Functional». Вещи, которые в C# мы будем проверять и логировать, в грамотном F#-коде просто не смогут существовать.
NIKOSV
07.11.2018 00:57Плюшки и синтаксический сахар это хорошо, но половину из того что вы перечислили в С# уже есть и активно добавляются в последующих версиях языка.
По поводу функциональщины, у меня был обратный опыт. Поигравшись со скалой где-от год (написав небольшой проект а не тупо туториалы), я лишь понял насколько я люблю С# и импиративщину. Иммутабельность, чистые функции, отсутствие состояния это все прекрасно, но оно и в С# доступно и обильно используется. Монады это тоже хорошо на первый взгляд, на простых примерах, но стоить копнуть глубже, написать что-нибудь сложнее и все, complexity побеждает здравый смысл. Вообще не покидало ощущение что у меня постоянно связаны руки тогда как импиративщиа и в частности С# дают полную свободу действий.Virviil
07.11.2018 02:21Полная свобода действий иллюзорна.
Можешь заставить программу делать все что хочешь, но не знаешь что ты он нее хочешь?
Потому что если знаешь — декларируй с помощью декларативного программирования. Главный плюс — ты автоматически доказываешь правильность ее работы.
Но есть и минус: иногда модель может быть слишком перегруженной для реальной программы. Тогда ты сделал очень много, но это никому не надо.
Короче говоря — надо балансировать.
Касательно же .net — как по мне, f# более выразителен. А "ООП"из него дергать просто.
0xd34df00d
07.11.2018 23:12Главный плюс — ты автоматически доказываешь правильность ее работы.
Это не в F# и даже не в хаскеле.
То есть, да, well-typed programs don't go wrong, но вот делают ли они то, что вы на самом деле ожидаете, зависит, и типизируемость в этих языках доказательством не является.
А там, где является, замучаетесь пыль глотать. Жизнь боль, арифметика неполна, тайпчекеры мысли не читают.
mayorovp
08.11.2018 08:51Напомню, в комментарии на который вы отвечали речь шла не о ФП или типизации, а о декларативности.
0xd34df00d
09.11.2018 07:05Вы напомнили, но у меня всё равно не щёлкнуло, почему это что-то меняет.
Но тут традиционно сначала придётся договориться о том, что мы собираемся доказывать и какая строгость доказательства нас устроит.
kagetoki Автор
07.11.2018 05:28Зачем жить с половиной слабореализованных фич, если можно получить полный набор?
Кроме того, про DU в сишарпе вообще пока ничего не слышал. Expression-based он тоже вряд ли станет когда-нибудь, верно? Обе эти фичи сильно способствуют стабильности и самодокументируемости кода.
geekmetwice
07.11.2018 03:05-2Ну хорошо, наглыми передёргиваниями и жонглированием специально подобранных примеров ты убедишь пару безусых студентов попробовать F#. СЕБЕ-то ты зачем врёшь, клоун?
BodukGenius
07.11.2018 04:56Как сборщик мусора смотрит на все это? Как вообще с быстродействием у функциональной парадигмы под CLR?
GrigoryPerepechko
07.11.2018 12:34immutable данные всегда приносили огромное кол-во копирования памяти.
Для скорости лучше писать просто на Сиchirkin
07.11.2018 14:33+2В большинстве случаев спасает переиспользование объектов, т.к. все композитные структуры включают друг-друга по указателю, а не по значению. Сборщики мусора могут быть реализованы совсем по-другому, когда известно что вся память иммутабельна.
Есть ещё всякие оптимизации типа линейные типов для минимизации расхода памяти (обсуждение для Haskell).
0xd34df00d
07.11.2018 23:15Генеративные сборщики мусора с nursing area, помещающейся в кеш и по факту выступающей аналогом стека, очень хорошо на это всё смотрят. Особенно на иммутабельность, ибо невозможно (пользователем, компиляторы умеют делать такие оптимизации, но они-то знают про это) проставить в старые данные ссылки на новые данные, что упрощает алгоритмы.
Олсо, есть всякие оптимизации вроде compact regions (если лень читать, можно просто посмотреть график 7 на с. 9). Правда, не под CLR, но неважно.
PhoenixUA
07.11.2018 04:57Powershell — всё сразу нужного типа и коротко :)
$employee = [pscustomobject]@{ Id = [guid]::NewGuid() Name = "Peter" Email = "peter@gmail.com" Phone = "8(800)555-35-35" HasAccessToSomething = $true HasAccessToSomethinElse = $false }
> $employee.Name.GetType().ToString() System.String > $employee.Id.GetType().ToString() System.Guid > $employee.HasAccessToSomething.GetType().ToString() System.Boolean
pawlo16
07.11.2018 07:25-3Тулинг, обилие библиотек и размер сообщества я сейчас в расчет не беру
Не берёте потому, что иначе пришлось бы рассказать про печальный для F# расклад с сообществом, тулингом и библиотеками, что свело бы нет восторженные интонации статьи. А именно:
— библиотек F# для типичных задач бизнеса практически нет, а те что есть маргинальные one man поделки, которые стрёмно использовать. Вот такой вот суперский язык F#, на котором почему то ни кто не хочет или не может написать ничего полезного. Но язык суперский. (поэтому юзаются С#-повские библиотеки, сводящие на нет все плюшки, описанные автором. Поскольку ни о каком ФП стиле и DSL в них естественно речи не идёт). Я уже молчу о таких прелестях, как депрекейтнутые версии либ для более старых версий net — у меня ни один проект F# под net 4 не собирается без ручной правки зависимостей
— туллинг так же отвратительный. Все .net-овские либы и фреймворки (asp, wpf, wcf, win, uwp, .NET Native) написаны для C# и VB, F# можно разве что с боку прикрутить. Для сборки проектов и управления зависимостями рекомендованы маргинальные утилиты, которые ни каким нормальным ide естественно не поддерживаются.
Внимание вопрос — а кому в 18-м году нужен язык сам по себе, без тулинга, библиотек и большого дружелюбного сообщества? Очевидно мазохистам и бездельникам, не решающим ни каких практических задач.
Далее, говоря о преимуществах ФП, автор забыл отметить недостатки:
— проблемы эффективности функциональных и неизменяемых структур данных
— неоправданный рост когнитивной нагрузки на чтение и понимание кода.
— функциональный код невозможно нормально отлаживать0xd34df00d
07.11.2018 23:38проблемы эффективности функциональных и неизменяемых структур данных
В самом худшем случае можно функционально и чисто писать в
ST
.
Или вспомнить про всякие там repa и accelerate.
неоправданный рост когнитивной нагрузки на чтение и понимание кода
Он всё равно меньше нагрузки из-за самой бизнес-логики (как и с правильным ООП).
Тут, впрочем, можно было бы довольно долго обсуждать, насколько легко писать правильный ООП-код или правильный ФП-код (ИМХО второе легче), но то такое.
функциональный код невозможно нормально отлаживать
А в чём у вас проблемы возникают?
pawlo16
09.11.2018 19:51А в чём у вас проблемы возникают?
А в том, что программа на F# — это последовательность инструкций, как и программа на С#. В C# любая инструкция доступна из отладчика, на ней можно ставить точку останова и заводить выполнение внутрь её тела. В F# благодаря таким прекрасным вещам как вычислительные выражения, цитирование, каррирование, кастомные операторы и проч. способы написать офигеть какой короткий и декларативный код — отладчик бессилен (на самом деле это не проблема, потому что F# практически ни где в реальной жизни не используется, соответственно и отладчик не нужен). Плюс в C# есть такая полезная опция, как реверсинг на лету и go-to-definition в библиотечный код, который так же можно дебажить. Программист на F# нервно курит в сторонке или рассказывает что дебагер нинуженSzer
09.11.2018 23:42отладчик бессилен
Эту чушь от тебя я уже опровергал в какой-то из прошлых статей про F#, где ты рассказывал то же самое.
Даже ссылку оставлю, освежить тебе память.
F# практически ни где в реальной жизни не используется, соответственно и отладчик не нужен
А ещё можно зажмуриться и повторять себе — F# не существует. Поможет.
Плюс в C# есть такая полезная опция, как реверсинг на лету и go-to-definition в библиотечный код, который так же можно дебажить. Программист на F# нервно курит в сторонке или рассказывает что дебагер нинужен
Декомпиляторов из IL в F# нет, это правда. Или я таких не знаю.
Нервно не курю, дебагером пользуюсь.pawlo16
10.11.2018 01:23Ваша истерика и хамоватый переход на «ты» неуместны. Вы бы лучше себя освежили, приняв успокоительное, и прочли написанное выше на свежую голову в уравновешенном состоянии. У меня ни где не сказано, что отладчика в F# нет в принципе или что он физически не может зайти внутрь функции — но пример ваш доказывает лишь это и ни чего более. Спорить с тем, что я не утверждал — глупо.
Речь о том, что промежуточное значение контейнера в цепочках вычислений с оператором |> в отладчике увидеть нельзя, в том числе и вашем примере — значение, которое получает Seq.iter скрыто от всех — и программиста, и отладчика.
Вот ещё пример точки останова, которая никогда не сработает
query{ for c in ctx.Main.ProductType do if c.GasName = "O?" || c.GasName = "NO?" then yield c.ProductTypeName.Value // здесь брекпойнт ставится но не работает } |> Seq.iter (printfn "%s")
А ещё можно зажмуриться и повторять себе — F# не существует. Поможет.
К вашим способам рефлексировать ни каких вопросов нет, жмурьтесь и повторяйте дальше. А мне достаточно сюда заглянуть чтобы сделать вывод что F# ни кому не нужен — из всех языков гитхаба только Elixir, fortran, haxe, racket и logos более мёртвые чем F#. Или коллег поспрашивать как они к F#-у относятся, может быть кто-то пишет на нём что-то полезное (нет, ни кто не пишет)
FoggyFinder
08.11.2018 11:12
lenferer
07.11.2018 07:43Добавьте вложенные ссылочные поля, и теперь ваш { get; } ничего не гарантирует — вы можете изменить поля этого поля
Автор наверняка Вы ведь слышали про инкапсуляцию, что Вам мешает в С# только в конструкторе переопределять поля, и сделать их приватными?a-tk
07.11.2018 12:44Если бы в C# реализовали primary constructors, который анонсировали, но не дотянули, было бы гораздо приятнее…
nkochnev
07.11.2018 08:53-2Спасибо за статью! Только начинаю изучать и вдохновляться F#
Но уже больно смотреть на старый код…
AxeLWeaver
07.11.2018 09:33Про синтаксический сахар новых версий C# уже писали в комментах? С каждой версией C# появляется всё больше и больше вкусного. А выбор ЯП — это на вкус и цвет, имхо…
fukkit
07.11.2018 10:58Всё это весело и красиво на детских 5-строчниках специально подобранных для одурманивания новобранцев.
Серьезные портянки функциональной тарабарщины по неэстетичности и мракобесию уступают только плюсовым шаблонам.
В конечном итоге после прочтения кода ты иногда понимаешь, что всё очень здорово друг с другом соотносится и взаимовытекает, но нихрена не понятно, что же все-таки оно делает и как.kagetoki Автор
07.11.2018 11:13Портянки тарабарщины трудны в прочтении независимо от парадигмы. А так — в каждой парадигме свои устоявшиеся абстракции, и если у вас есть несколько лет опыта в ООП, но в ФП его гораздо меньше — не стоит удивляться, что когнитивная нагрузка для вас субъективно выше. Это вовсе не значит, что все воспринимают ФП так же, как вы.
А вообще в вашем утверждении можно поменять "функциональной" на "оопэшной", и суть не изменится — субъективщина без аргументов.
GrigoryPerepechko
07.11.2018 13:16Просто признайте что программирование — не для вас
NIKOSV
09.11.2018 00:38Я когда изучаю новый язык (основной С#), параллельно пытаюсь написать реальный проект. И вот у меня имеется два схожих проекта один на scala, другой на go. Когда через пол года возвращаешься к scala проекту — ничего не понятно, тупо пару дней восстанавливаешь воспоминания. Когда через пол года возвращаешься к go проекту — часик полистать код и можно дальше в бой. Написание этого проекта на scala тоже заняло в разы больше времени. Вывод: простота решает.
anonymous
07.11.2018 11:50+1А посоветуйте, пожалуйста, какую-нибудь книгу по F# для начинающих
FoggyFinder
07.11.2018 14:51+1Тут стоит уточнить, кого вы имеете ввиду под начинающими. Тех у кого есть опыт программирования на C#, тех кто знакомых с каким-то не функциональным языком, но не имеет представления о .NET или тех, кто только пробует делать свои первые шаги в программировании?
anonymous
07.11.2018 15:02+1Имел в виду знакомых с C#, но незнакомых с F#
FoggyFinder
07.11.2018 15:16+1Я бы посоветовал книгу "Программирование на F#". С тех пор F# изменился, но база — основные типы и сам функциональный подход все тот-же и описывается очень понятным и доступным языком.
Также есть восхитительный сайт F# for fun and profit. И, конечно, присоединяюсь к рекомендации worldbeater — "Domain Modelling Made Functional". Сам я эту книгу еще не читал, но это как раз тот случай когда можно советовать "не глядя"
AndreySitaev
07.11.2018 14:27Автор, не опускайте ООП-аудиторию до уровня hello-world-щиков. Прямо платоновский диалог, где одна сторона — тупица, задающий «учителю» удобные вопросы по шпаргалке. «А звали ФП-программиста Альберт Эйнштейн».
Факт в том, что современные яООП (от C# до Javascript) сильно преобразились за последние годы, впитав в себя многие ценные фишки ФП. C# может и в лямбды, и в каррирование, и в иммутабельность (struct, readonly уже отменили?).
Примеры, что вы приводили (в частности, if (… ) {… } else {… }) — рука-либо. С таким «глубоким» пониманием C# и таким изысканным стилем кодирования, который вы приписываете абстрактному ООП-кодеру, мне кажется, в ФП ему рано.
Или вот, скажем,
Name: string VS public string Name { get; set; }
Нас волнует краткость? Тогда зачем нам свойства, там, где, можно предположить по логике автора, достаточно public — переменной? Да, модификатор public остается — беда….
Из разряда — «шах и мат, аметисты» (уже не к автору):
Такое почему-то тоже
var a = 0; var b = flag ? { Console.WriteLine("true"); a = 1; } : { Console.WriteLine("false"); a = 2; };
…
Так что тернарный оператор в C# конечно уступает expression-based if из F#
Учите матчасть:
var a = 0; (flag ? new Action(() => { Console.WriteLine("true"); a = 1; }) : (() => { Console.WriteLine("false"); a = 2; })).Invoke();
allcreater
07.11.2018 14:47+1Контрпример, вроде Action'ов в тернарном операторе выглядит как-то не очень, хотя, в принципе, вполне работоспособен и решает поставленную задачу. Собственно, речь вроде бы и шла о лаконичности подобного(пусть и странного) кода, а не отсутствии возможности запилить альтернативу.
Но фиг с ними, синтетическими примерами. Надо боевой код сравнивать.AndreySitaev
07.11.2018 15:08+1Над лаконичностью всегда можно поработать. Например:
var _ = flag ? Ternvoke(() => { ... }) : Ternvoke(() => { ... }); ... public static Action Ternvoke(Action a) { a(); return a; }
Но, думается, некоторая «избыточность» кода C# в сравнении с F# картины ничуть не портит. А вот будет ли нетривиальная бизнес-логика столь же «читаема» в коде F#? Как ни крути, в продакшн не обойтись без сонма классов, без многоуровневой иерархии объектов в домене приложения…
Потому — соглашусь с апелляцией к «боевому» коду. Мне интересно было бы почитать разбор мало-мальски сложных проектов на F#, решающих задачи «реального» мира.Szer
07.11.2018 15:29+3И тут можно наступить на родовую болезнь C# — отсутвие unit/Void.
Перегрузить Ternvoke для работы с Func<> не получится (вход можно, а выход — нет).
Пример из продакшна. В пайплайнах есть функция
tee
, которая просто делает сайдэффект над передаваемым аргументом и возвращает его же.
F#:
let tee f x = f x |> ignore x
C# :
public static class Extensions { public static T Tee<T>(this T x, Action<T> f) { f(x); return x; } }
Предположим у нас есть функция
saveToDb: 'a -> bool
(принимает что-то, сохраняет в бд, возвращает успешно или нет)
F# пережовывает что угодно:
"abc" |> tee Console.WriteLine |> tee saveToDb |> ...
C# уже не очень:
"abc" .Tee(Console.WriteLine) .Tee(saveToDb)//упс, компайл еррор ...
Added:
А было бы неплохо объявить на C# так:
public static T Tee<T>(this T x, Func<T, _> f)
Но увы
Szer
07.11.2018 15:43+1Но, думается, некоторая «избыточность» кода C# в сравнении с F# картины ничуть не портит. А вот будет ли нетривиальная бизнес-логика столь же «читаема» в коде F#? Как ни крути, в продакшн не обойтись без сонма классов, без многоуровневой иерархии объектов в домене приложения…
Потому — соглашусь с апелляцией к «боевому» коду. Мне интересно было бы почитать разбор мало-мальски сложных проектов на F#, решающих задачи «реального» мира.Обойтись без сонма классов очень даже можно. Отказываться от них полностью бессмысленно, они полезны. Но строчить классы для ВСЕГО — так же глупо, как всё засовывать в чистые функции.
Про избыточность C# по сравнению с F# неплохо сказано в свежих докладах Антона Молдована и Вагифа Абилова:
https://www.youtube.com/watch?v=4eXthLWzYrk
https://www.youtube.com/watch?v=4x9slVi_RBo
Сам пишу прод на F#, экономит тонну времени.
pawlo16
07.11.2018 19:52Про избыточность C# по сравнению с F# неплохо сказано в свежих докладах Антона Молдована и Вагифа Абилова:
Если вы не в состоянии показать избыточность C# по сравнению с F# в нескольких словах на понятном языке, значит либо избыточности нет, либо вы не владеете вопросом.FoggyFinder
07.11.2018 20:31Если вы не в состоянии показать избыточность C# по сравнению с F# в нескольких словах на понятном языке, значит либо избыточности нет, либо вы не владеете вопросом.
Есть еще варианты кроме перечисленных вами.
В статье был приведен простейший пример, на всякий случай скопирую его снова:
C#:
public class Employee { public Guid Id { get; } public string Name { get; } public string Email { get; } public string Phone { get; } public bool HasAccessToSomething { get; } public bool HasAccessToSomethinElse { get; } public Employee(Guid id, string name, string email, string phone, bool hasAccessToSmth, bool hasAccessToSmthElse) { Id = id; Name = name; Email = email; Phone = phone; HasAccessToSomething = hasAccessToSmth; HasAccessToSomethinElse = hasAccessToSmthElse; } }
type Employee = { Id: Guid Name: string Email: string HasAccessToSomething: bool HasAccessToSomethingElse: bool }
Для определения одного и того-же объекта в F# варианте затрачивается меньше времени. В рамках одного типа разница не столь значительна, но все изменится когда вы увеличите их количество в разы.
И это только самый тривиальный случай. F# код намного лаконичнее чем аналогичный на C#.
kagetoki Автор
07.11.2018 15:39+2Хорошо, пример с
if/else
неудачный, черт с ним.
Меня тут несколько раз уже носом ткнули в это, вы не первый. Как насчет всего остального, что есть в статье?
Далее.
readonly
? Хорошо, но кода надо написать все еще гораздо больше, чем в F#.
И это важно, потому что это влияет на решение разработчика о том, как он будет писать код. Все предпочитают двигаться по пути наименьшего сопротивления, все хотят писать меньше кода для решения одной и той же задачи.
У разработчика есть выбор:
1) Написать обычную изменяемую структуру, которая всем понятна и является дефакто стандартным решением этой задачи, набравX
символов
2) Написать неизменяемую структуру, набрав3X
символов, объяснять на код ревью, почему было принято такое странное решение, писать больше кода каждый раз, когда хочешь поменять 1-2 поля в ней, и, скорее всего, научитьJsonConvert
парсить ее, потому что дефолтного конструктора нет.
Кто в здравом уме выберет второй вариант?
Не забывайте, написать на C# неизменяемую структуру на принцип, чтобы доказать кому-то что-то, это совсем не то же самое, что писать так каждый день на работе.
То же самое относится к вашему "учите матчасть". Так сделать можно, но никто так делать не будет.
Что насчет неизменяемых коллекций в C#? Да, есть
Collections.Immutable
, но их все ругают за тормознутость, я ни в одном проекте не видел их использование.
Кстати,
Equality & Comparison
все еще надо самостоятельно реализовывать.
Покажите, пожалуйста, как выглядит каррирование на C#. Я честно не знаю.
AndreySitaev
07.11.2018 16:16-1Каррирование — была тема на том же Хабре:
public static Func<A, Func<B, R>> Carry<A, B, R>(this Func<A, B, R> f) { return a => b => f(a, b); } Func<double, double, double> log = (n, b) => Math.Log(b, n); var log3 = log.Carry()(3); var c = log3(9);
kagetoki Автор
07.11.2018 16:25Ну вы же понимаете, что это непрактично для продакшна? А если мне нужно другое количество аргументов? А если это не
Func
, aAction
?
Сравните теперь с каррингом в F#, где он нативный, и делать не надо ничего вообще.
let inline add x y z = x + y + z let add5 = add 5 let add5then7 = add5 7
TheShock
07.11.2018 16:45+1На самом деле вопрос скорее в том, зачем нужно каррирование на C# да и вообще, зачем оно необходимо на практике. Обычно написать полную лямбду — значительно более читабельное, хоть и менее хайповое решение. Ну то есть, например, у нас есть функция (даже, представим, что у нее удобно для каррирования идут аргументы, что редко)
Math.Log(base, n);
Можно написать через карирование. Ну, например, как-то так:
var log10 = Math.Log.Carry(10)
Но, значительно более читабельный вариант — такой:
var log10 = n => Math.Log(10, n)
Конечно, вариант с каррированием на пару символов короче (на два, если быть точным), но, без сомнения, хипстота на следующем афтепати будет в восторге. А если аргумент в правильном порядке, а не специализированном под ФП, то как тогда?
Math.Log(n, base); // => var log10 = n => Math.Log(n, 10) // => var log10 = Math.Log.Carry(???)
Даже в F# используется лямда в таких случаях. Так зачем вообще каррирование, если код без него более ясный и универсальный?pawlo16
07.11.2018 18:06+2На самом деле вопрос скорее в том, зачем нужно каррирование на C# да и вообще, зачем оно необходимо на практике.
Потому что явная запись, без каррирования — это не кашерно. Не вписывается в концепцию EDD (Emotion-Driven Development). Естественно речь идёт об эмоциях узкого круга лиц.
0xd34df00d
07.11.2018 23:44На самом деле вопрос скорее в том, зачем нужно каррирование на C# да и вообще, зачем оно необходимо на практике.
Смотря как вы смотрите на функции.map f
— это не map, частично применённая к f, это просто функция, которая принимает список и возвращает список с f, применённой к каждому элементу. Если у вас
setName :: String -> Record -> Record
тогдаsetName "meh"
— функция, которая устанавливает имя в «meh».
Но каррировать всегда и везде, конечно же, не нужно.
А если аргумент в правильном порядке, а не специализированном под ФП, то как тогда?
`log` 10
даст вам нужную функцию. Если мы говорим об ФП вообще, конечно.TheShock
07.11.2018 23:49Объясните, почему? Вот есть функция
log(n: number, base: number)
Разве`log` 10
не подставит в качестве аргумента n, хотя надо base?0xd34df00d
07.11.2018 23:54Это хаскель (и идрис, и агда, и подобные языки, а то я F#-то не знаю).
Бэктики делают из функции с префиксным синтаксисом вызова псевдооператор с инфиксным синтаксисом вызова (т. е. вместо
log n 10
вы пишетеn `log` 10
). А дальше — ну, вас же не смущает, что(/ 2)
— функция, делящая на два?
И я там в комментарии дописал ещё про «зачем».
mayorovp
08.11.2018 09:10Смотря как вы смотрите на функции. map f — это не map, частично применённая к f, это просто функция, которая принимает список и возвращает список с f, применённой к каждому элементу.
Но отличается это от частичного применения только в том случае, когда map объявлена как принимающая ровно один аргумент.
`log
` 10 даст вам нужную функцию. Если мы говорим об ФП вообще, конечно.А вот это уже частичное применение функции, а не каррирование.
0xd34df00d
08.11.2018 18:26Тогда я вообще не понял исходного вопроса о пользе или вреде каррирования.
TheShock
08.11.2018 19:54+2Я не говорил, что он вредный или полезный. Я не вижу в нем особого смысла просто.
0xd34df00d
09.11.2018 07:06А. Ну, каррирование упрощает частичное применение первого аргумента (или начального отрезка списка аргументов), и этот случай оказывается достаточно распространённым, на самом деле. Нередко в библиотеках есть даже функции типа «
bar
— this is justfoo
with arguments swapped».
skiedr
08.11.2018 00:43Как пример, каррирование позволяет прозрачно получить dependency injection.
mayorovp
08.11.2018 09:11Но необязательно же для этих целей делать его автоматически или вводить по умолчанию в языке для всех функций.
skiedr
08.11.2018 17:08-1Я же не говорил, что только для этого. Мне нравится этот слайд в докладе про f# twitter.com/jeroldhaas/status/535919819355598848
TheShock
08.11.2018 18:11+2Процитирую старый ответ на эту лживую картинку. Ну вот серьезно, перестаньте ее цитировать, это показывает вас с очень плохой стороны
Ваше сравнение паттернов с ФП — манипулятивная картинка для глуповатых программистов. Чистой воды демагогия. Специально для хипстоты, которая любит кричать: «ааа, ооп — такое глупое, не то, что фп», но при этом не очень любит думать.
Точно такой же бред можно сочинить и в обратную сторону:
— Функции высшего порядка / Класс
— Каррирование / Класс
— Композиция функций / Класс
— Предикат / Класс
— Функтор / Класс
— Лифт / Класс
— Моноид / Класс
— Монада / Класс
И так далее из списка «жаргона ФП»
Паттерны — это просто примеры некоторых общепринятых подходов, при помощи которых можно решать задачи. И таких не существует разве что в тех парадигмах, которыми никто не пользуется.
Особо смешно, что сперва автор приводит эту картинку, которая показывает, как все просто в ФП решается просто функциями, а потом зачем-то пишет статью с кучей практик (их можно назвать даже паттернами, лол). Странно, что у него мозг не взрывается от такого диссонанса. Шапку в мороз носите и выбирайте теплую, а не красивую.nsinreal
09.11.2018 12:15Самое интересное: если сходить и посмотреть сам доклад, то станет ясно, что слайд имел как минимум развлекательную направленность. А еще можно увидеть, что слайд показан не весь: в конце показывается текст «Seriously, FP patterns are different». Человек просто хотел сказать, что нельзя переносить опыт из ООП в ФП.
nsinreal
09.11.2018 14:50Каррирование нужно для лучшей поддержки pipeline-оператора |>
1) имя «n» — полохое. Лучше либо никакое, либо нормальное
2) уже на пятерке строк pipeline у вас начнет рябить в глазах
В C# аналогичный синтаксический сахар — это static extension методы.
mayorovp
07.11.2018 16:35-1и, скорее всего, научить JsonConvert парсить ее, потому что дефолтного конструктора нет
И как же эту задачу будет решать F#? :-)
Что насчет неизменяемых коллекций в C#? Да, есть Collections.Immutable, но их все ругают за тормознутость, я ни в одном проекте не видел их использование.
Вы так пишите, как будто неизменяемые коллекции F# работают как-то принципиально по-другому...
kagetoki Автор
07.11.2018 17:53F# имеет аттрибут
[<CLIMutable>]
. Он позволяет мутацию, но не из вашего F# кода.FoggyFinder
07.11.2018 18:12и, скорее всего, научить JsonConvert парсить ее, потому что дефолтного конструктора нет
Мне тоже не совсем понятно что вы имели ввиду. Какой конструктор использовать можно указать с помощью
[JsonConstructor]
/[<JsonConstructor>]
. К тому-же у структур есть конструктор по умолчанию.
anonymous
07.11.2018 17:17Есть ли примеры сложных GUI приложений?
FoggyFinder
07.11.2018 17:45Сложные приложения вряд-ли многие захотят выкладывать в открытый доступ.
WPF работает исправно. Полная F# поддержка для UWP скорее всего будет не скоро, но положительные сдвиги в этом направлении уже есть. Если по каким-то причинам вам нужен WinForms, то можно использовать, например, TrivialBehinds раз уж нет конструктора форм.
Xamarin.Forms поддерживает F#, для более удобной работы есть дополнительные библиотеки, такие как Fabulous, который здесь уже не раз упоминался.
pawlo16
07.11.2018 19:38+1Сложные приложения вряд-ли многие захотят выкладывать в открытый доступ.
Тем не менее в открытом доступе овердофига десктопных UI приложений на C# и почти ни одного на F#. Совпадение?
WPF работает исправно
WPF в F# ?? Вы в данном случае себя обманываете или окружающих?
VS не содержит шаблона проекта F# для WPF.
Xaml редактор не умеет генерировать F# код и привязывать его к разметке.
Если у вас вся исправность работы F# с WPF сводится к прикручиванию сборки, написанной на F#, так это смех.FoggyFinder
07.11.2018 20:20Тем не менее в открытом доступе овердофига десктопных UI приложений на C# и почти ни одного на F#. Совпадение?
Нет, не совпадение. Не очень понимаю что именно вы хотите оспорить. C# намного, намного более популярный и востребованный язык чем F#. OpenSource разработчику десктопного UI приложения (да и не только десктопного) не выгодно писать на F#, просто по той причине, что проект не встретит той поддержки, которую мог бы получить аналогичный на C#. Все это должно быть достаточно очевидно. Но раз уж зашла речь об очевидных вещах, то если кто-то и задумается над написанием WPF приложения на F#, то он должен быть готов к тому, что хороших материалов не так уж и много.
WPF в F# ?? Вы в данном случае себя обманываете или окружающих?
Ни себя, ни окружающих я не обманываю. Вы просто говорите о другом. Поддержка F# в VS значительно уступает поддержке C# и проблема отсутствия шаблона для WPF далеко не в вверху списка того, что было бы здорово добавить.
Xaml редактор не умеет генерировать F# код и привязывать его к разметке.
Вы абсолютно правы в том, что поддержки для code-behind в F# проекте в том виде в котором она есть для C# нет, просто по той причине что в F# нет такого понятия как
partial
.
Если у вас вся исправность работы F# с WPF сводится к прикручиванию сборки, написанной на F#, так это смех.
Вы ошибаетесь. Хотя нет ничего смешного в том, чтобы определить View часть в C# проекте, тогда как все остальное переложить на F#.
pawlo16
07.11.2018 21:32Не понял при чём здесь внешняя поддержка. Нормального языка, тулинга и экосистемы более чем достаточно, чтобы вывести проект на уровень, достаточный для того, чтобы заинтересовать потенциальных контрибъютеров. По моему опыту использования F# в нём ничего этого нет, потому и проектов на нём нет в количестве, достойном упоминания. Хоть язык и существует достаточно давно, и в VS он есть из коробки.
Вы абсолютно правы в том, что поддержки для code-behind в F# проекте в том виде в котором она есть для C# нет, просто по той причине что в F# нет такого понятия как partial.
Отсутствие partial конечно осложняет, но не делает задачу невозможной. Если бы они очень захотели, то придумали бы врапер со ссылкой на реализацию (как вариант). Либо производили бы кодогенерацию в определённое место исходного файла, обозначенное тегами в комментариях. Вариантов много, но это ни кого не интересует — инвестиции в такой функционал не окупятся. Поэтому не проще ли честно признать, что вот этот вот вариант с десктопным UI на F# — маргинальщина и отстрел ног? Я пробовал если что.
нет ничего смешного в том, чтобы определить View часть в C# проекте, тогда как все остальное переложить на F#
Не всегда приемлемо держать модели в отдельном проекте. Кроме того модели на практике вряд ли будут в ФП стиле, потому что должны быть совместимы с сугубо ООП-шными вещами на подобие IPropertyChanged. То есть писать всё то же ООП, но только не C#, а на F#, чтобы потом связать его из С#. Это несколько странно.FoggyFinder
07.11.2018 22:00По моему опыту использования F# в нём ничего этого нет, потому и проектов на нём нет в количестве, достойном упоминания.
Можно узнать по-подробнее о вашем опыте с F#? Впервые встречаю человека с реальным опытом с F# и который бы утверждал что "F# не нормальный язык".
Отсутствие partial конечно осложняет, но не делает задачу невозможной. Если бы они очень захотели, то придумали бы врапер со ссылкой на реализацию (как вариант).
Возможно, конечно. Даже FsXAML предоставляет совсем базовую поддержку для code-behind.
Поэтому не проще ли честно признать, что вот этот вот вариант с десктопным UI на F# — маргинальщина и отстрел ног?
Проще, да, но не честнее. Код на F# пишется быстрее. Даже для демонстрации проблемы или наоборот решения какого-то вопроса использовать его намного удобнее. В тех случаях когда не требуется красивый и сложный UI с большим количеством code-behind возможностей F# точно хватит.
Не всегда приемлемо держать модели в отдельном проекте.
Разве? Вы не могли бы привести примеры?
Кроме того модели на практике вряд ли будут в ФП стиле, потому что должны быть совместимы с сугубо ООП-шными вещами на подобие IPropertyChanged.
В том то и дело что используется другой подход. F# в первую очередь функциональный язык программирования, а не чисто функциональный. Если лучше и удобнее применить другой подход, то его и стоит применять.
pawlo16
09.11.2018 13:22-1Можно узнать по-подробнее о вашем опыте с F#?
Опыт приличный, и он достаточно стандартный для F# — от малого к бОльшему. Изучил в 12 году будучи начинающим программистом на волне интереса к ФП, доверия к втирающим про world bank type provider и из-за вещей, которых в C# на тот момент не было, в частности, async и MailboxProcessor. Область применения — круд сервисы в монолитных проектах и вебморды. В данный момент инспектирую чужой код и переписываю F#-лигаси монолит на другой язык программирования — думаю понятно какой. Были определённые ожидания, связанyые с websharper и ui.next, но к сожалению они так и не вышли из маргинальной ниши. Все остальные фреймворки (например hopack, suave, fable как самые изветсные), как показала практика, можно выкидывать, не дочитав readme. В чём любой желающий может легко убедится: вместо документации — имитация оной, 1.2 контрибьютера, поддержка околонулевая, шизофреничные стрелочки в качестве кастомных операторов, в продакшене нет, количество звёздочек на гитхабе красноречиво намекает что проект никому не нужен
Код на F# пишется быстрее.
При этом читается хуже, потому что нет жестких гайдлайнов, развитых линтеров и единого форматирования. F# — типичный write-only ЯП. Возвращаться к своему коду тяжело, читать чужой невыносимо больно — вне зависимости от уровня квалификации того, кто этот код написал. Ну и работат код на F# значительно медленнее, чем C#.
В тех случаях когда не требуется красивый и сложный UI с большим количеством code-behind возможностей F# точно хватит.
Если задача проста, то и F# соёдёт и даже иногда C++.
Не всегда приемлемо держать модели в отдельном проекте.
Разве? Вы не могли бы привести примеры?
Усложняется сборка. Линтеры не могут почекать имена пропертей. А иногда модели вообще не нужны, достаточно старого доброго smart ui.
F# в первую очередь функциональный язык программирования, а не чисто функциональный. Если лучше и удобнее применить другой подход, то его и стоит применять.
Если лучше и удобнее применить ООП, то лучше и удобнее применить C#, поскольку F# в планее ООП не даёт ничего сверх C#. object expression — это мелочи и легко решается закрытыми вложенными классами.Szer
09.11.2018 13:55+1Не надо свой опыт 6-ти летней давности втирать людям как актуальный.
- Linter — fantomas
- Тулинг — обычный Net Core SDK (dotnet build и погнали). F# компилятор нынче в него включён, ничего ставить не надо, если неткор стоит.
- Про отсутствие доков, это конечно же прогон. В Hopac дока просто гигантская. Giraffe (на мой взгляд самый перспективный веб фреймворк, т.к. поверх asp.net core) прекрасно документирован.
- Скорость кода F# медленнее C#? У меня другие данные
Если что, pawlo16 известный в комьюнити F# хейтер. К его словам надо с пудом соли относиться. Ходит по всем F# статьям на хабре и рассказывает как он когда-то не осилил, а Go эт просто космос :)
pawlo16
09.11.2018 14:59-2Ходит по всем F# статьям на хабре и рассказывает как он когда-то не осилил
А как на счёт показать где это я рассказывал что не осилил F#?
Расскажите какие именно мои высказывания в топике про F# вы интерпретировали как «Go эт просто космос»
Не надо свой опыт 6-ти летней давности втирать людям как актуальный.
Вот только опыт не шестилетней, а недельной давности. Не надо выдавать свои фантази за факты. Перечитайте написанное выше, может с n-ого раза дойдёт
Linter — fantomas
для тех, кто пробовал R#, это не линтер, а одно лишь название. Про какую ещё пионерскую поделку расскажете?
В Hopac дока просто гигантская.
так у вас качество доки определяется количеством строк? ну-ну. Вы ещё расскажите о том какая в fable хорошая дока, посмеёмся
Скорость кода F# медленнее C#? У меня другие данные
Видимо левель у вас низковат, иначе бы вы не сравнивали синт. тесты с боевым кодом. С чего бы F# быть медленнее на синтетических тестах, если он почти в точности переписан с C# и использует в основном Array и mutable?
FoggyFinder
09.11.2018 14:07Были определённые ожидания, связанyые с websharper и ui.next, но к сожалению они так и не вышли из маргинальной ниши.
Сам я далек от веб-разработки, но WebSharper с самого начала показался несколько странным.
Все остальные фреймворки (например hopack, suave, fable как самые изветсные), как показала практика, можно выкидывать, не дочитав readme.
Вы, наверное, вместо Hopack хотели написать Giraffe?
Можно уточнить что вы имеете ввиду под "как показала практика"?
В чём любой желающий может легко убедится: вместо документации — имитация оной, 1.2 контрибьютера, поддержка околонулевая, шизофреничные стрелочки в качестве кастомных операторов, в продакшене нет, количество звёздочек на гитхабе красноречиво намекает что проект никому не нужен
С документацией да, есть некоторые проблемы. Но сообщество достаточно активное чтобы быстро получить ответ на искомый вопрос.
В продакшене Suave используется, хотя сейчас, насколько мне известно, большую все популярность набирает SAFE.
Что касается звездочек и контрибуторов, то это все есть в открытом доступе:
Suave: 1015 Star / 86 contributors
Fable: 1,437 Star / 90 contributors
Giraffe: 939 Star / 46 contributors
Я уверен, что здесь достаточно больше количество звездочек чтобы считать заявление, цитирую "проект никому не нужен" безосновательными. Решительно не понимаю почему вы прибегаете к подобной риторике.
При этом читается хуже
Дело вкуса или привычки. Мне, например, читать F# код значительно проще чем C#. И тут дело не в самом синтаксисе, а в строгом порядке файлов. В F# все зависимости четко прослеживаются, тогда как в C# проекте крайне сложно ориентироваться в виде хаотичного расположения файлов.
При этом читается хуже, потому что нет жестких гайдлайнов, развитых линтеров и единого форматирования.
Жестких нет, но есть рекомендации
The F# Component Design Guidelines
F# style guide
Возвращаться к своему коду тяжело, читать чужой невыносимо больно — вне зависимости от уровня квалификации того, кто этот код написал.
Не нужно распространять ваш негативный опыт на всех
Усложняется сборка.
Сложно представить проект в котором модель и UI смешаны в одном проектном файле по причине сложности их сборки. Можно увидеть ссылки или статьи подтверждающие ваши слова ?
А иногда модели вообще не нужны, достаточно старого доброго smart ui.
Мне такое сложно представить :-)
Если лучше и удобнее применить ООП, то лучше и удобнее применить C#, поскольку F# в планее ООП не даёт ничего сверх C#.
Абсолютно
pawlo16
09.11.2018 18:28-2Сам я далек от веб-разработки, но WebSharper с самого начала показался несколько странным.
Так потому и показался что далёк.
Вы, наверное, вместо Hopack хотели написать Giraffe?
Нет, я имел ввиду Hopac.
Я уверен, что здесь достаточно больше количество звездочек чтобы считать заявление, цитирую «проект никому не нужен» безосновательными
А я нет. Например, Suave делают 2 человека, отнюдь не 86. Для сравнения беру первый пришедший в голову проект на Го — маргинальный, ведомый одним человеком на досуге потехи ради, исключительно под windows desktop, хайпа ноль — 2900 stars. Потому что проект полезен в своей нише. И вот таких либ, знаете ли, много. А Suave существует давно, обсуждается в сообществе весьма активно, на волне некоторого хайпа мог бы и больше звёздочек собрать. Впрочем, не навязываю, это субъективная оценка.
Мне, например, читать F# код значительно проще чем C#
А приходилось ли вам инспектировать чужой код на F#?
в C# проекте крайне сложно ориентироваться в виде хаотичного расположения файлов
Не разу не сталкивался с такой проблемой. Обычно иерархия модулей проекта определяется архитектурой и понятна из названий каталогов и файлов проекта. Хаос означает, что были использованы кривые абстракции, либо забили на DI, либо архитектурные косяки. И, наоборот, постоянно думать о порядке файлов в проекте страшно выбешивает.
Не нужно распространять ваш негативный опыт на всех
Что плохого в том, чтобы предостеречь начинающих от некачественного инструмента? Тут у нас вроде бы место для дискуссий, и даже можно высказывать мнения, не совпадающие с автором статьи. Или об F# либо хорошо, либо ничего, как о покойнике (в комьюнити именно так и принято, благо нет возможности сжигать еретиков на кострах)?
Сложно представить проект в котором модель и UI смешаны в одном проектном файле по причине сложности их сборки. Можно увидеть ссылки или статьи подтверждающие ваши слова?
Вам сложно понять почему один проект лучше чем два при прочих равных? тогда я сдаюсь. Модель, которая постоянно меняется в процессе разработки, и чтобы из основного проекта увидеть изменения эти изменения, нужно пересобрать вспомогательный. Если вас это совершенно не напрягает, то у меня ни каких вопросов нет.FoggyFinder
09.11.2018 19:56Для сравнения беру первый пришедший в голову проект на Го
Сравнение неуместно, при чем здесь любой другой язык?
А Suave существует давно, обсуждается в сообществе весьма активно, на волне некоторого хайпа мог бы и больше звёздочек собрать
У F# репозитория всего 1,406 звезд. Хотя тут наверное организация сыграла роль, так как F# Core находящийся в FSharp организации намного более популярен — 177.
А приходилось ли вам инспектировать чужой код на F#?
Приходилось. Кроме того, мой первый PR был сделан как раз в F# библиотеку.
Обычно иерархия модулей проекта определяется архитектурой и понятна из названий каталогов и файлов проекта
Да, но если речь идет об OpenSource проекте, то разбираться в чужом коде, чтобы исправить баг или даже просто локализовать проблему бывает проблематично (время-затратно)
И, наоборот, постоянно думать о порядке файлов в проекте страшно выбешивает.
Не нужно думать о порядке файлов, он автоматически линейный.
Что плохого в том, чтобы предостеречь начинающих от некачественного инструмента?
Вы меня превратно поняли. Я лишь попытался вас предостеречь от громких безосновательных утверждений. В вопросе полноты изложения — указывать не только на плюсы, но и на недостатки я вас полностью поддерживаю, но хотелось бы что-бы они были обоснованы.
Я действительно благодарен вам за ответы.
(в комьюнити именно так и принято, )
Насколько я могу судить, в F# сообществе принято говорить правду.
Модель, которая постоянно меняется в процессе разработки, и чтобы из основного проекта увидеть изменения эти изменения, нужно пересобрать вспомогательный.
Shrug. Все-таки не на C++ пишем, сборка занимает не так много времени.
Ryppka
07.11.2018 22:09+1Мне кажется, что писать на C# практичнее, во всяком случае UI. А на F# — приятнее, а логику, трансформации и парсинг — и практичнее. Люблю ML (не машинное обучение который, а семейство языков) в любом виде.
tangro
09.11.2018 13:32Если бы F# по сравнению с С# давал хоть 1 доллар выгоды при реализации типичного бизнес-проекта, то бизнес его гонял бы уже в хвост и в гриву на каждом первом проекте. А если не даёт — значит написать на нём практически полезный код будет дольше. А значит — нельзя говорить, что язык «просто лучше».
Возможно, язык приятнее для программиста, но мы знаем, что приятное далеко не всегда полезно.
dark_ruby
Очередной беребезчик, добро пожаловать в функциональный лагерь, у нас есть плюшки.