… на основе обзорного тура и аж одной программы. Ну ладно, еще есть паттерн railway oriented programming, с которым я познакомился довольно давно, и в котором примеры на этом языке. При этом с С# у меня опыта чуть побольше, правил наш замечательный толстый клиент и в студенчестве даже делал приложение для Windows Mobile (в те времена, когда использовался термин “коммуникатор”). Так что не стоит ждать от этой заметки чего-то глубокого или интересного: уровень гораздо ниже, чем у хабровской стрельбы в ногу с Kotlin.

Если есть желание быстренько ознакомиться с синтаксисом, но лень читать даже обзорный тур, то есть F# за 60 секунд.

Установка и среда разработки #

Сам dotnet устанавливается через snap, особых проблем не возникло. Напряг момент с телеметрией, которая по умолчанию собирает много чего. К счастью, можно ее отключить через переменную окружения (или отключится только уведомление, это надо еще проверить:) ) , но первые N запусков с ней уже были. И узнать о ней можно либо из документации (которую я разумеется прочел уже потом) или после первого запуска. В общем, в бесплатном приложении товар — это пользователь, классика.

Дальше начались пляски с VS Code. Вроде как из каждого утюга хвалят, да и альтернатив не особо много. Скачал, поставил, установил рекомендуемые плагины. Сгенерировал hello world и успешно запустил его. Решил поправить немного — а мне VS Code такой: а у тебя F# не установлен.

Ладно, чешу репу, использую 2 стандартных приема… И все равно не могу запустить даже стандартный hello world. Оказывается, надо было воспользоваться “простой” инструкцией и отредактировать руками пару файлов проекта VS Code. Я, конечно, все понимаю, это блокнот с наворотами и все такое, но для обычного шарпового приложения пришлось бы пройти через такой же путь, а это продукты одной компании! Вроде в 2021 году живем, чтобы руками файлы билдов править — спасибо хоть, что они не в формате Makefile.

Изучал все не в один присест и когда после перерыва продолжил писать код — запуск опять перестал работать, но по другой причине: VS Сode не мог понять конфигурацию coreclr. Предлагал поставить расширение, чтобы ее поддерживать, и единственный кандидат в каталоге расширений — это “extension for PeachPie - the PHP compiler for .NET and .NET Core”. W T F. Удалил все плагины, перезапустил VS Сode, установил заново расширения — не работает. В интернетах советуют… переустановить плагины. Сделал еще раз те же действия — заработало. W T F.

С точки зрения самой разработки — есть какая-то подсветка, которая ощутимо подтормаживает. Автодополнение есть, но не очень впечатлило, подсовывает фигню какую-то (очень субъективное ощущение, но как есть). Иногда раскрываются выведенные типы, но они тормозят вместе с подсветкой.

В общем, я получил больше негативных эмоций, чем удобства разработки. Гемора много, а толку мало.

Интересности #

Весь язык описывать не буду, опишу вещи, которые показались интересными, странными или важными.

Синтаксис #

Отступы и значимое форматирование как в питоне ­— на первый взгляд прикольно. Однако даже с этим код местами все равно может выглядеть стремно, даже в туре есть такие места (например, вторая секция кода в разделе паттерн-матчинга). При этом иногда можно через ; записать несколько выражений в одной строке (хорошо хоть, не всю программу). Некоторые источники говорят, что скобки и точки с запятой “не засоряют” код — имхо, это сильное преувеличение, использовать их все равно нужно, и я бы не сказал даже, что редко.

Часто торчит наследие C и C#: например, “процентные” форматы вывода в sprintf. При этом в сравнениях используется непривычный синтаксис: <> вместо != и not вместо ! — кто-то не любит восклицательный знак. А вот and и or это по-прежнему && и ||.

Объявления функций похожи на Haskell’овские. Однако любую рекурсивную функцию надо явно помечать — это как-то тухло и непонятно зачем. Еще стремно, что для лямбд используется ключевое слово function (хорошо хоть его можно сократить до fun). Функция без аргументов неотличима от обычного выражения (и это хорошо), поэтому для грязных функций без аргументов надо явно передавать unit (скобочками).

Для классов используется странный синтаксис, но что-то в этом есть: меня заинтересовала идея, когда реализация интерфейса явная и локализованная. Вместо override для всех методов нужно написать, мол, реализую интерфейс такой-то: этот метод так, этот сяк. Хотя это стоит пощупать: может, на бумаге хорошо, а в реальности будет как с проверяемыми исключениями в Java.

Понравилось ключевое слово use для ресурсов. Достаточно инициализировать/открыть ресурс где надо, а закроется он сам при выходе из текущей области видимости — никаких дополнительных оборачиваний не нужно.

Классная фича — единицы измерения на уровне синтаксиса языка, причем для системы СИ (которая международная система единиц) все единицы уже определены. Был бы я студентом и писал бы решение какой-нибудь типовой задачи по физике — обязательно попробовал бы.

Работа с коллекциями #

Списки объявляются через точку запятой: [1; 2]. Альтернатива — по элементу на строку, т.е. разделитель — перенос строки. Судя по всему логика тут в том, что ; и перенос строки “взаимозаменяемы” (на самом деле нет). А запятая — разделитель кортежа, т.е. [1, 2] — это то же самое, что и [(1,2)].

Все полезные функциональные методы — статические. Никаких тебе someList.filter someLambda, вместо этого надо писать List.filter someLambda someList. Когда речь идет о преобразующих функциях — это еще ладно, но вот когда приходится писать Seq.head generator вместо generator.head — это уныло. Местами даже проще было использовать низкоуровневый итератор (Enumerator). Вообще статические методы — это грустно, поменяешь тип коллекции — и всю цепочку переписывать. При этом extension-методы есть, шаблонные типы есть — вроде ничего не мешает реализовать абстрактные map, filter и т.д. Может, это “так исторически сложилось” (extension-методы были в F# не всегда), может, система типов недостаточно мощная, но от этого приятнее не становится.

Есть сахарный метод конвеера, |>: подобно unix’овому pipe |, с его помощью можно передавать результат выполнения предыдущей функции в следующую. Например

[1;2;3] |> List.filter (fun x -> x % 2 <> 0) |> List.map ((*) 5)

выдаст [5; 15]. Можно еще сделать композицию функций через >>, но применимость ее в цепочках, на мой взгляд, сомнительна.

Массивы определяются через интересную комбинацию [| |]. Разочаровало, что они по умолчанию изменяемые, хотя списки — иммутабельные. Неконсистентно, однако.

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

Немного чужеродно выглядит обычный for, когда есть итераторы. Местами читается как SQL-style LINQ, т.е. погано.

Особый интерес вызывают имена некоторых методов. Как вам, например, foldBack2, intersectMany, map3, mapi2, zip3 (когда нет zip2), sortInPlaceBy vs sortInPlaceWith vs sortWith? Еще позабавило, что в ответах на SO иногда рекомендовали использовать LINQ для некоторых преобразований, особенно если начало или конец — C#-коллекция.

Алгебраические типы данных #

Реализованы интересно, но странно, что нет из коробки перечисления и строкового представления (они по факту POCO). Забавно, что произведение типов можно записать через умножение.

Single-case Discriminated Union — с одной стороны прикольно, а с другой — это просто альтернативный синтаксис для record, как POCO с одним полем, который особо нового ничего не привносит. Для снятия обертки предлагают использовать паттерн-матчинг, стандартного метода класса вроде как нет.

Взаимодействие с C# #

При вызове чего-то шарпового возникает немного вырвиглазная ситуация: часть методов (C#) в UpperCamelCase, а часть (F#) — в обычном camelCase. А стандартная библиотека общая, так что этого не избежать.

С# список и F# список не совместимы: попробовал создать шарповый, чтобы потом по нему map’ом пробежаться и меня постигла неудача. Надо конвертировать через Seq.toList.

Некоторые источники утверждают, что в F# есть null-safety. Это, увы, ЛПП: это гарантируется только в “чистом” F#, вызвал любую С# библиотеку (в т.ч. стандартную системную) — жди беды или обрабатывай явно.

Итого #

Какой-то киллер-фичи не нашел (всякая асинхронщина и ФП сейчас почти в каждом утюге). Не могу сказать, что язык удобный, да и с консистентностью у него есть проблемы. Без C# тяжело что-то написать. Вообще напоминает ситуацию со Scala. В ней тоже сильна ФП-парадигма, многие вещи приходится делать через Ж…аву, причем интероп иногда фиговый, свои коллекции, да и изначальное позиционирование было как “ФП на существующей экосистеме”. Но у Scala есть какая-никакая ниша, работа с коллекциями там приятнее (особенно в 2.13/3.0), сам язык логичнее, экосистема богаче и нет большого брата с телеметрией. В общем, в F# есть интересные идеи, но использовать его я, конечно, не буду :)