November 2021

S M T W T F S
 123456
78910111213
1415161718 1920
21222324252627
282930    

Style Credit

Expand Cut Tags

No cut tags
Monday, June 2nd, 2014 05:23 pm
На правах потока сознания, преимущественно чтобы сформулировать. Но если кому из понявших, о чем речь, есть что сказать - welcome.

Читая тут на досуге "Higher Order Perl", обнаружил, что автор запутался между List::Util::reduce из стандартного модуля и тем, что в более других языках называется fold. И до меня, похоже, дошла небанальная мысль, откуда там путаница. Я буду в хаскельных терминах, с аннотацией типами.

Итак, у нас есть (для ясности будем работать с левым fold, как более близким по смыслу к тому, что нам надо)

foldl :: (acc -> elt -> acc) -> acc -> [elt] -> acc

и есть его частный случай, когда типы аккумулятора и элемента списка совпадают. Его хорошо применять для вычисления сумм, произведений, и т.п. барахла, которое является моноидом относительно подставленной операции (и следовательно, есть единица оного моноида). fold - тотальная функция относительно списка. Собственно, если посмотреть в исходники, то мы увидим, что sum и product определяются именно как частичное применение foldl:

sum, product :: (Num a) => [a] -> a
sum = foldl (+) 0
product = foldl (*) 1

И есть очень, на первый взгляд, похожий блок функций minimum, maximum, etc.:

minimum, maximum: (Ord a) => [a] -> a

которые, сюрприз, определяются совсем не так, потому что у операций сравнения единицы нет, и поэтому второй аргумент для foldl взять неоткуда. И вот они-то, вернее, лежащий под ними

foldl1 :: (a -> a -> a) -> [a] -> a

и соответствуют List::Util::reduce. А путаница оттого, что они очень похожи, но имеют совершенно разную природу. Похоже, авторы Haskell тоже попались в эту ловушку, ибо иначе непонятно, почему foldl1 называется именно так. В чем разница в природе?

Вышеуказанный тип для foldl1, так похожий на частный случай для foldl (acc = elt, и откуда-то добыт второй аргумент) дает частичную функцию, не определенную для пустого списка. И всё, что на нем построено - minimum, maximum, minimumBy, etc. - тоже частичные. А каков будет тип тотальной? Правильно,

foldl1total :: (a -> a -> a) -> [a] -> Maybe a

И сюрприз, сразу видно, что это - не частный случай foldl. Невозможно так подставить типы и частично применить foldl, чтобы получить такой тип.

В теории, можно еще свести это к частному случаю foldl1, если взять acc = Maybe a, в качестве дефолтного значения Nothing, и похимичить с функцией. Но и с точки зрения стратегии вычислений, и с точки зрения смысла операции, это будет плохая идея. Хорошая идея - это

foldl1total f (x:xs) = Just (foldl f x xs)
foldl1total _ _ = Nothing

но это не частный случай foldl, это использование foldl глубоко внутри определения (перед ним - pattern matching, для которого надо вычислить список до конструктора, а после - инкапсуляция результата). Что совсем не то же самое. И сюрприз, именно так, с точностью до типа результата (пустота вместо Just и error вместо Nothing) и определяется foldl1. То есть на самом деле по реализации видно, что foldl1 не является foldl. А по типу - нет, и народ путается. И неважно, что perl не типизированный. Существенная разница в смысле операций от этого никуда не девается, и идея объединить эти две операции в одну неизбежно приводит к проблемам.

Надо сказать, что из такого определения foldl1 есть интересные следствия. Pattern match, как уже говорилось - это такая проверка, с предварительным вычислением до конструктора. Казалось бы, можно определить minimum как

minimum = foldl1 min

Но сюрприз, реальное определение толще:

minimum [] = errorEmptyList "minimum"
minimum xs = foldl1 min xs

Почему? Да потому что функция частичная, т.е. с вероятностью 1/2 :) мы налетим на то, что она не определена, и хочется, чтобы в эпитафии написали хотя бы, какой именно функцией программист пристрелил всю свою программу (если он вызывал minimum, то хотелось бы увидеть сообщение именно про нее, а не про foldl1, которую он не вызывал). То есть разобрать список до конструктора приходится дважды - сначала в minimum при проверке, затем еще раз в foldl1 уже для работы (и заодно для проверки тоже).