Комментарии 56
Замечательно! Это очень вкусная тема — комбинаторный грамматический разбор. Не планируете ли вы продолжения, раскрывающего прочие достоинства аппликативного подхода: например, возможность статического анализа, невозможного в монадических парсерах. Я спрашиваю, поскольку сам подумывал написать об этом, но вдруг, у вас уже готов материал.
В этой статье стоит сказать, что при вашем определении типа Parser
возможен автоматический вывод экземпляра класса Funtor
для него. Либо очень компактный и симметричный "ручной вариант":
instance Functor Parser where
fmap f = Parser . (fmap . fmap . fmap) f . unParse
использующий то обстоятельство, что и функция, и список и пара являются функторами. В этом сила и смысл классов типов.
Я старалась избегать в примерах таких конструкций, их красоту трудно понять новичкам, оно скорее может отпугнуть. Но надеюсь, что добравшиеся до комментариев осмыслят и оценят этот вариант, тут вся сила языка просматривается. С такими небольшими красивыми этюдами можно даже отдельный пост делать.
Затем, чтобы разобраться, что происходит. Проще всего использовать derive Functor
, он выводится однозначно. StateT это верное решение, но не для туториала, придется слишком много тащить лишней инфраструктуры. Бесточечное определение, может быть, и выглядит колдовством, но оно отражает смысл преобразования: разворачиваем, заходим в "на дно" функтора, он у нас трехуровневый, и заворачиваем. При этом становится видно, что мы не делаем ничего лишнего.
Ни разу нигде не видел переменную типа больше одной буквы. Вот что, например, значат переменные типа в конструкции data SnapletInit b v, почему именно b и v а не a и z?
Скорее всего это пришло из теории категорий. И сам язык располагает к использованию компактного синтаксиса, инфиксных синонимов и коротких имён в том числе.
Но надо сказать, что многобуквенные переменные типа не так уж и редко встречаются.
showVsPretty :: (Show a, PrettyPrint a) => a -> (String, String)
причина как раз вполне понятна. Название этого параметра не несёт никакой смысловой нагрузки и лишь выступает в качестве местозаполнителя. И вряд ли особая польза была бы, если это записывали как:
showVsPretty :: (Show itemToShow, PrettyPrint itemToShow) => itemToShow -> (String, String)
А вот почему внутри реализаций функций такая тяга к коротким переменным — загадка. Вероятно как и в случае с C причина кроется в возрасте и наследственности языка. Во времена становления Haskell и уж тем более во времена становления ML о читаемости кода ещё так не пеклись, и люди просто привыкли.
Если вы мне хотели показать какой haskell понятный, то вы сделали строго противоположное.
applyP :: Parser (a -> b) -> Parser a -> Parser b
applyP (Parser p1) (Parser p2) = Parser f
where f s = [ (sx, f x) | (sf, f) <- p1 s, -- p1 применяется к исходной строке
(sx, x) <- p2 sf] -- p2 применяется к строке, оставшейся после предыдущего разбора
Что такое sx? Что такое p2 и зачем он, если есть p1? Что такое s? что такое x? Что такое f?
(Я вижу 'where', но мне всё равно не понятно что вы подразумевали под f в рамках вашей предметной области).
Да, если что, задача решается проще. Можно сделать композицию из f g h s d m l и всё получится. Что такое 'm' в данном примере вы вполне сами догадаетесь по контексту d и l.
Здесь имена обладают такими особенностями:
* то что это «какой-то парсер» понятно и так — из конструкции (Parser p1) и из объявления типа.
* кроме того, что это какой-то парсер, в этом контексте про переменную ничего не известно — мы не можем её назвать там «bankAccountNumberParser». А имена «parser1» и «p1» несут примерно поровну информации (примерно нисколько — то, что это парсер, и так понятно).
* область видимости переменной — три строки. Если метод разрастётся до многих десятков строк, то лучше пусть будет «parser1», т.к. к середине метода читатель уже забудет, что видел конструкцию (Parser p1) в начале.
Вот имена «sx», «sf», имхо, выбраны не очень удачно. Но сходу не предложу значительно лучших имён.
Ещё тут зря используется одно имя «f» для разных переменных: одна для имени парсера, а другая в рамках list comprehension. Мой вариант:
applyP :: Parser (a -> b) -> Parser a -> Parser b
applyP (Parser p1) (Parser p2) = Parser $ \str ->
[ (p2remain, f a) | (p1remain, f) <- p1 str, -- p1 применяется к исходной строке
(p2remain, a) <- p2 p1remain] -- p2 применяется к строке, оставшейся после предыдущего разбора
(up: заменил совсем абстрактный икс на «a», так мне кажется лучше, потому что имя переменной совпадает с именем тИповой переменной — так понятно, что это то самое «а», которое в типе функции).
Искусство придумывать названия — второе сложное искусство в IT.
В том смысле, что нам про них в данном контексте ничего не известно — это «какой-то парсер» и «какой-то ещё парсер».
А тип виден в первой строке, енкодить его в имя переменной — излишне при небольшой области видимости.
Имхо.
Например, это может быть inner parser или outer parser. Имена переменных, функций, классов и модулей — это самое важное в языке программирования.
Это требование не из CS, это требование из software engineering.
Когда вы пишете достаточно абстрактный библиотечный код (как в примере в статье), то очень часто оказывается, что вам почти или совсем не важно, что скрывается за переменными. В таком случае вы никогда не сможете придумать осмысленные имена. Ну, предположим, вы пишете функцию, складывающую два числа (именно не сумму на счёте с поступившими деньгами, а вообще, функцию сложения для стандартной библиотеки). Как вы назовёте её параметры? «слагаемое» и «прибавляемое»? seriously? :)
В математике и среди программистов, пишущих абстрактный код, этот принцип несущественности назначения переменных (аргументов функций) настолько распространён, что есть противоположная максима: point-free notation, т.е. способ написания уравнений / кода, при котором имена переменных элиминируются полностью. Пример из хаскель-вики:
it is clearer to write
let fn = f . g . h
than to write
let fn x = f (g (h x))
(имена fn, f, g и h в данном случае не обсуждаем). В данном примере нам настолько не важно, что такое «x», что мы его вообще предпочитаем не упоминать.
Disclamer: всем и так очевидно, что прикладной код пишется гораздо чаще, чем абстрактный библиотечный.
Я бы сказал, что в этом месте у нас плохой язык. Почему? Потому что я вижу, что f, .g, h в этом примере — это объекты из списка. Для списочных данных (у каждого из которых нет имени) должны быть функции списочные же.
Что-то вида: let chained_func = map(apply_in_chain, func_list)
Для любителей значков: let chained_func = . func_list
.
Если кто-то в коде вам напишет:
b = a[0] + a[1] + a[2] + a[3] + a[4] + a[5] + a[6]
Вы же его за это поругаете, правда? Так почему же для серийных объектов первого порядка (функий) такое исключение?
it is clearer to write
let getAuthorSurname = getSurname . getAuthor . getBookById
than to write
let getAuthorSurname id = geSurname (getAuthor (getBookById id))
Здесь «id» это какой-то идентификатор книги; в данном контексте нам не важно, откуда мы его достали и чему он равен; просто айди. Мы можем попытаться придумать более лучшее название, чем «id», потратить на это час и изобрести «anyBookIdentifier». Или мы можем вообще выкинуть его из кода.
Software engineering подразумевает, что код будут читать и перечитывать. И аннотации в виде осмысленных имён — это совершенно обязательное для промышленного кода.
Мне кажется, вы прицепились к самому простому, тривиальному и неинтересному вопросу в программировании. Дискуссия вышла длинная, но с контексте статьи и CS ни о чем. Столько же времени и слов можно было бы потратить на что-либо другое.
По поводу одинаковых названий переменных типа и переменных в коде, такое лично меня запутывало на ранних стадиях обучения (и не только меня, видела ещё пару студентов с такой проблемой буквально в этом семестре). Начинает казаться, что тут типы first-class. Скорее всего это тоже дело вкуса, но с тех пор сама так не пишу.
loopCounter — это по ушам за это бить. Потому что "счётчик в цикле" нам и так видно. А что этот цикл считает?
Вот, смотрите:
for i, v in enumerate(g(d)):
f(v, i)
Сравните с:
for sequential_num, emploee_name in enumerate(get_emploees(department)):
print_numbered_bage(emploee_name, sequential_num)
В каком случае вы понимаете что я имел в виду?
P.S. Я думал про имя счётчика долго. Это моя третья попытка придумать выразительное имя. Вот если у нас бейджи нумерованные, что такое "номер на бейдже"?
А во втором — конкретная бизнес-логика.
Именно это вам и пытаются донести.
Не надо использовать слово «бизнес-логика». Я предпочитаю «семантика».
Что, если кто-то предпочитает иначе?
def add(a, b):
return a+b
С
def add_two_abstract_items(first_abstract_item, second_abstract_item):
return first_abstract_item + second_abstract_item
Вы действительно считаете что второй вариант лучше?
Что эта функция делает? Кто её использует?
Это можно было бы написать с помощью 2 вложенных циклов, если бы они были в Haskell. Тут достаточно настроить свой внутренний интерпретатор и научится читать декларативный код Haskell. В первую очередь обращать внимание на аннотацию типов, а само данное выражение проще читать справа-налево и снизу-вверх, а не наоборот.
А вот часть списка определённых в ней функций: hackage.haskell.org/package/lens-4.17/docs/doc-index-60.html
Не встречала ещё тех, кто был бы к этой библиотеке равнодушен, её либо любят и везде используют (ведь названия операторов такие очевидные и интуитивно понятные), либо не используют никогда и другим не советуют.
Тут две строчки. Всего две короткие симметричные строчки, все обозначения определены тут же. Типы объясняют что делает функция, компилятор подтверждает, что реализация верна. Да, нужно остановиться ненадолго, но с этим можно разобраться, и этот опыт будет полезен. И это не APL, не J в двух строчках на которых можно сломать ногу, здесь из абстракций только генерация списка.
Аппликативные парсеры на Haskell