Я хотел изучить что-то экзотичное, но в то же время практичное. Критерии практичности на тот момент (несколько лет назад) критерии были такие: среда должна уметь создавать обособленные исполнимые модули, содержащие консольные и графические приложения, работающие под управлением Windows XP и более поздних версий на 32-битной платформе. Если этого минимума нет, язык не рассматривался, так как ничего полезного для себя я на нём всё равно не смог бы написать (на своём тогдашнем ноуте).
Это было нужно для собственного развития. Мне захотелось понять на практике, что такое функциональные языки программирования, так как до этого я изучал только императивные языки, а о функциональных имел представление на уровне формального определения. Этот пробел хотелось заполнить. Factor мне к тому времени уже подвернулся, презентация на
Google Tech Talks произвела хорошее впечатление, а на
Википедии было сказано, что язык функциональный, стековый и многопарадигмальный. Это отлично вписалось в идею расширить горизонты моего знания: функциональный - есть, а заодно ещё и стековый, что бы сие ни значило.
Хаскель в те времена ещё только зарождался, Лисп не был практичным по моим критериям, а из прочих языков вообще было не ясно, как выбрать что-то стоящее. Фактор же не только установился и сразу заработал, но и имел богатейшую библиотеку "из коробки", встроенную документацию, интерактивную разработу и многое другое.
Что мне сразу не понравилось после КП и подобных. Текст ошибок, выдаваемых компилятором, иногда не имеет отношения к допущенной ошибке. Эта проблема известна и привычна, я думаю, работающим на C++, но после Delphi и КП есть привычка точно понимать, что пошло не так. В Факторе нет такого строгого синтаксиса, поэтому компилятор сам часто не понимает, что вы от него хотите, от этого взаимный конфуз. В этих случаях приходится переходить на разработку маленькими шагами, чтобы нащупать проблемное место.
Вы спросили об опыте использования. Чтобы мои оценки имели контекст, опишу свой сегодняшний уровень. Я использую Фактор в основном в качестве хобби, для домашних проектов, но иногда пишу и скрипты, которые автоматизируют что-то на работе. За годы знакомства я достиг уровня, когда могу написать средних размеров модуль без ошибок компиляции, но регулярно приходится подглядывать в справку, чтобы уточнить порядок параметров в стандартных функциях или детали их использования. Я работал с GUI (виджеты на OpenGL, таблицы), с файлами, БД (sqlite), многопоточностью (green threads, IPC по TCP), криптографией, делал бинды к сторонним библиотекам (sodium, ...), портировал чужие алгоритмы на Фактор (Ryu). До сих пор не могу сказать, что код на Факторе я напишу быстрее аналогичного кода на Delphi, но я всё ещё надеюсь, что такой переломный момент настанет. В тех случаях, когда в Факторе есть подходящая библиотека (а там их очень много), небольшие проекты удаётся написать за приемлемое время - даже быстрее, чем на Delphi, где надо было бы что-то искать либо изобретать самому.
Пример. Вот скрипт, который можно запустить в корневом каталоге проекта, и он сконвертирует все файлы, у которых нет BOM, из Latin-1 в UTF-8 с BOM.
Код:
! Copyright (C) 2017 Alexander Ilin.
USING:
io io.directories.search io.files io.encodings.8-bit.latin1
io.encodings.string io.encodings.utf8 io.pathnames
kernel namespaces sequences
;
IN: sources-to-utf8
: utf8-signature ( -- str )
{ 0xEF 0xBB 0xBF } utf8 decode ;
: file-to-utf8 ( filename original-encoding -- )
dupd [ contents ] with-file-reader
dup "" head? [ 2drop ] [
swap utf8 [ utf8-signature write write ] with-file-writer
] if ;
current-directory get { ".pas" ".inc" ".dpr" ".txt" }
find-files-by-extensions [ latin1 file-to-utf8 ] each
В своих текстовых редакторах я не нашёл такой фукнции пакетной конвертации. Если немного понимать базовые принципы, то код этот достаточно прост и хорошо читается.
Вот пример, который читается не так легко. Это было написать быстрее, чем в очередной раз изобретать проход по подкаталогам в Delphi:
Код:
! Copyright (C) 2019 Alexander Ilin.
USING:
formatting
io.directories io.directories.search io.encodings.utf8 io.files
io.pathnames kernel math namespaces sequences unicode
;
IN: lower-case-extensions
: except-.git ( seq -- seq' )
[ "/.git/" swap subseq? ] reject ; inline
: (lower-case-extensions) ( path -- )
[ dup length swap last path-separator? [ 1 + ] unless ] keep
recursive-directory-files except-.git [
over tail dup [ parent-directory ] [ file-stem ] [ file-extension ] tri [
dup >lower 2dup = [ 5drop ] [
nip "git mv -f %s %s%s.%s\r\n" printf
] if
] [ 3drop ] if*
] each drop ;
: lower-case-extensions ( path batch-file-name -- )
utf8 [ (lower-case-extensions) ] with-file-writer ;
: run-in-current-directory ( -- )
current-directory get "lower-case-extensions.cmd"
lower-case-extensions ;
MAIN: run-in-current-directory
Этот скрипт использует recursive-directory-files для получения списка файлов, начиная с текущего каталога, и создаёт файл "lower-case-extensions.cmd". В результирующем файле содержатся команды git-mv для замены расширения файлов на строчные. Понадобилось, когда увидел, что у нас часть файлов имеет расширение ".Pas" вместо ".pas".
Общее впечатление таково, что при разработке на Факторе я больше времени трачу на "вылизывание" кода, приведение его к наиболее компактному виду, устранению дублирования и т.п. Это доставляет мне удовольствие как решение головоломки. В этом смысле язык, с одной стороны, как раз подходит для хобби, а с другой стороны это просто говорит о том, что я ещё нахожусь в процессе его освоения, раз решения не "выстреливают" из головы сразу. Здесь действительно очень своеобразная языковая модель, и при программировании нужно думать существенно иначе, чтобы писать оптимально. Конечно, никто не запрещает писать в императивном стиле, с локальными переменными, в инфиксной нотации и т.п., но какой в этом смысл? Для меня смысл как раз в том, чтобы взглянуть на привычные и простые задачи по-иному, с совершенно новой точки зрения.
Есть задачи, которые очень легко переносятся из естественного языка на Фактор, и такой код очень легко читается. Например, "5 days ago" - это валидный код на Факторе, который вернёт дату-время за 5 суток до текущего системного времени. Аналогично можно написать библиотеки к любым постфиксным нотациям - "5 cm", "15 m", "20 km", etc., и они будут очень естественно встроены в язык.
Также естественно решаются задачи потоковой обработки данных - например, всё то, что в консоли мы написали бы через пайпы "|", в Факторе записывается точно так же, только через пробелы: применяем одну функцию, результат передаём следующей, и так далее по цепочке. При этом число входных и выходных параметров не ограничено, но строго проверяется компилятором. Без этой последней проверки я вообще не понимаю, как можно написать что-то вменяемое - на том же Форте и миллионе его клонов.
Когда-то очень давно, чуть ли не в школьные или ранние университетские времена, была у меня мысль изобрести некий язык программирования, который бы позволил урезать число потенциальных ошибок до минимума. Что-то вроде библиотеки шаблонов алогоримов над Delphi. Например, нужно нам пройти по массиву от начала до конца, мы берём шаблон прохода по массиву, и внутрь дописываем то, что нам нужно сделать с каждым из элементов. Обычно в Delphi мы применяем некий идиоматичный код типа "for i := 0 to Length(TheArray) - 1 do ...". Проблем с таким подходом много: необходимо уникальное имя для переменной i, саму эту переменную надо где-то объявить, идиома не идентифицируется человеком, который с ней не знаком, в ней можно допустить ошибку, да и вообще, как правило, довольно много текста вокруг нужно прочитать и критериев проверить, чтобы убедиться, что это именно та самая идиома, и что никакие её критерии и правила не нарушены. А если нарушены, то это специально или по ошибке? Не ясно.
Использование шаблона алгоритма решает все эти проблемы, поскольку шаблон однозначно и легко идентифицируется по имени и не даёт нарушить своей структуры, так как структура эта описана в другом месте, а не в месте использования. Кроме того, шаблон может быть улучшен следующим образом: "for i := Low(TheArray) to High(TheArray) do ...". Исправил шаблон - и автоматически новая версия используется везде. Исправить идиому не так просто, особенно в крупном проекте, в том числе потому, что её не всегда легко идентифицировать/найти.
Я с удивлением обнаружил, что в Факторе это всё уже есть и работает. Можно написать любые шаблоны, дать им имя и использовать из библиотек. Например, проход по всем элементам массива (и любой последовательности): "[ ... ] each". Цикл от 0 до X-1: "X <iota> [ ... ] each". При этом анонимной функции внутри [] будут последовательно передаваться значения счётчика от 0 до X-1, и нет ни проблемы с созданием локальной переменной с уникальным именем, ни проблемы с тем, что прикладной код может испортить её значение и сломать цикл, и т.п.
Однако, <iota> - это конструктор неизменяемой виртуальной последовательности с элементами от 0 до X-1.
Код:
! Integer sequences
TUPLE: iota { n integer read-only } ;
ERROR: non-negative-integer-expected n ;
: <iota> ( n -- iota )
dup 0 < [ non-negative-integer-expected ] when
iota boa ; inline
M: iota length n>> ; inline
M: iota nth-unsafe drop ; inline
INSTANCE: iota immutable-sequence
Означает ли это, что будет создан экземпляр объекта в динамической памяти, у которого итератором будут вызываться виртуальные методы получения длины и очередного элемента (с проверкой диапазона индекса) и всё это будет дико тормозить? Вот, что думает на этот счёт оптимизирующий компилятор:
Код:
[ 5 <iota> [ drop ] each ] disassemble
00000184779bbe60: 89059a5103ff mov [rip-0xfcae66], eax ! Предохраняемся от зацикливания.
00000184779bbe66: b805000000 mov eax, 0x5 ! Число итераций.
00000184779bbe6b: 31db xor ebx, ebx ! Обнуляем счётчик цикла.
00000184779bbe6d: 4983c610 add r14, 0x10 ! Какой-то пролог.
00000184779bbe71: e909000000 jmp 0x184779bbe7f (( gensym ) + 0x1f) ! Прыжок на проверку условия выхода из цикла.
00000184779bbe76: 48ffc3 inc rbx ! Увеличиваем счётчик цикла.
00000184779bbe79: 8905815103ff mov [rip-0xfcae7f], eax ! Предохраняемся от зацикливания.
00000184779bbe7f: 4839c3 cmp rbx, rax ! Проверяем условие выхода из цикла.
00000184779bbe82: 0f8ceeffffff jl dword 0x184779bbe76 (( gensym ) + 0x16) ! Если не закончили, возвращаемся в цикл.
00000184779bbe88: 4983ee10 sub r14, 0x10 ! Какой-то эпилог, тут я не спец.
00000184779bbe8c: 89056e5103ff mov [rip-0xfcae92], eax ! Предохраняемся от зацикливания.
00000184779bbe92: c3 ret ! Выход из функции.
Код, может быть и не такой чистый, как написанный вручную, но суть в том, что все неиспользуемые абстракции удалены. Во все циклы компилятор автоматически добавляет команду записи в определённую страницу памяти. Если программа зациклилась, её можно прервать, пометив эту страницу как read-only. Я лично добавил поддержку Ctrl+Break в платформенный код под Windows по аналогии с BlackBox.
Программа никогда не "зависает" при долгой работе с вводом-выводом, поскольку все файловые операции сделаны через "overlapped", соответственно, другие кооперативные треды продолжают работать, в том числе отрисовка интерфейса.
Сочетание высокоуровневого кода с достаточно эффективной компиляцией меня очень подкупает. Есть даже
проект по использованию языка в микроконтроллерах. Кроме того, прилагающаяся библиотека очень богата, и там есть чему поучиться. Иногда поражаюсь, насколько крутые штуки реализованы в модулях на полстраничке-страничке кода. Исходники на Факторе очень компактные, но достаточно читабельные, хотя об этой читабельности приходится специально заботиться и уделять ей время, продумывая разделение функций на именованные куски.
Другой пример полезного класса шаблонов - функции типа with-file-reader: "path encoding [ ... ] with-file-reader". Для сравнения примерный аналог на Delphi:
Код:
var
f: File;
begin
AssignFile(f, path);
ResetFile(f);
try
...
finally
CloseFile(f);
end;
end;
Другими словами, with-file-reader берёт на себя открытие и закрытие файла, в том числе при возникновении исключений внутри пользовательского кода. Очень удобно и компактно. В Факторе много различных with-* на все случаи жизни. Я позаимствовал этот подход, и уже на Delphi стал писать процедуры, которые принимают на вход анонимную функцию и оборачивают её специфичной обработкой исключений или примитивами синхронизации.
Код:
type
AccessResourceProc = reference to procedure (const Data: TDataRecord);
procedure TClass.AccessResource(Proc: AccessResourceProc; const MaxWaitTime: Cardinal = INFINITE);
begin
if Mutex.Acquire(MaxWaitTime) then
try
Proc(FInternalDataStructure^);
finally
Mutex.Release;
end;
end;
// Usage:
MyClassInstance.AccessResource(
procedure (const Data: TDataRecord)
begin
... // Handle Data.
end,
100); // Wait not more than 100 msec.
При таком подходе и Mutex, и FInternalDataStructure остаются скрытыми от посторонних глаз, и доступ к внутренним данным класса TClass возможен только через синхронизанию. Соответственно, забыть вызывать Mutex.Acquire или Mutex.Release у клиентского кода попросту нет никакой возможности, что повышает надёжность кода.
GUI в Factor организовать труднее, чем в Delphi, это недостаток, который в том числе обусловлен кросплатформенной работой поверх OpenGL (стандартные контролы ОС не используются). Конечно, никто не мешает сделать бинды и пользоваться, но в стандартной поставке этого нет (хотя, что-то там из GTK маячило, но я не вчитывался), зато есть свои собственные контролы и механизмы их расположения на окнах. В целом, очень зрелый инструмент для прикладного программирования, на мой взгляд.