> Да и как удобный синтаксис может мне вдруг навредить завтра — тоже не очень ясно.
В общих чертах-то ясно. Плохой код должен выглядеть плохо. То есть идея в том, что глядя на вставку в середину массива, человек должен заподозрить неладное и убедиться, что оно к месту. Скажу за хаскель. Если бы писать там с мутабельными переменными было так же удобно, как без них, хаскель был бы другим. Вопрос только в том, что именно счесть плохим кодом, и вот тут да, мнения разнятся. Время покажет.
Почему? Если программисты напишут программы, заменяющие программистов, то программы, заменяющие других людей будут писать уже эти программы, а не сами программисты.
Отчего ж, есть и mapM и forM, но можно и так тоже.
Есть даже такое :)
ghci> mapMOf_ (each . _Right . each . _head) putChar [Right ["foo", "baz", "baz"], Left 10, Right ["x", "y", "zoo"]]
fbbxyz
Взять каждый (each) элемент, затем пойти внутрь конструктора Right (_Right) (при этом откинув те, что Left), затем взять каждый элемент (each), и затем первую букву (_head)
И для них выполнить putChar
Ну да ладно, это так, к слову.
Хм, вы имеете ввиду оптимизацию на уровне компилятора? Тогда я не очень понимаю её суть — насколько я понимаю, композитная функция `(f. g)` всё равно транслируется в машинный код как последовательное применение двух функций к каждому элементу. Или здесь можно ещё что-то соптимизировать?
Как я понимаю, суть в том, что вместо того, чтобы применить g, затем сделать, грубо говоря, yield, а затем ещё и f, можно сразу f.g применять. Насчёт чего-то ещё, не знаю, это уже более низкоуровнево. Но это был просто пример, суть которого в том, что даже в своей библиотеке можно задавать такие правила, заменяющие какие-то случаи на более эффективные реализации. Правда, тут всё на совести того, кто такие правила задаёт, хотя теоретически можно было бы ещё и требовать эквивалентности такой замены.
Вот, например, из документации:
So, for example, the following should generate no intermediate lists:
array (1,10) [(i,i*i) | i <- map (+ 1) [0..9]]
К сожалению, глубоко тему не рыл, но вот, например: www.randomhacks.net/2007/02/10/map-fusion-and-haskell-performance
Стоит также отметить, что если в случае итераторов всё понятно, они специально так заделаны, то с двумя подряд foreach так уже сделать нельзя:
for x in xs:
f(x)
for x in xs:
g(x)
# Совсем не то же самое, что:
# for x in xs:
# f(x)
# g(x)
А если мы хотим, чтоб можно было — придется переезжать на итераторы, тогда как в хаскеле что foreach, что «итераторы» делаются одной конструкцией.
Написал несколько сумбурно, но, надеюсь, донес мысль :)
Нуу, тут хоть и две строчки, но специальных фич Haskell-а целая куча — и ленивые вычисления, и рекурсия через фиксированную точку, и развитая система типов.
Ну так разумеется, ведь мы же именно то, к чему приводят эта ленивость, чистота и развитая система типов, и обсуждаем.
Я просто пытался донести мысль, что эти вещи нельзя мысленно просто перенести на другой язык и посмотреть, потому что они дают больший эффект, когда язык под них заточен.
В общем и целом, мы с вами вроде во всём согласны, а этот code golf начинает отнимать время, поэтому если критических возражений нет, предлагаю на этой оптимистической ноте и закончить.
У вас ужасный, ужасный, ужасный стиль программирования на Python
Это не «стиль», это пример, показывающий, что в императивном языке такая замена неэквивалентна, а потому она не является оптимизацией, она является деталью реализации, о которой программисты должны знать, чтобы не совать в map грязные функции.
Как я уже говорил, если кто-то применяет функции с побочными эффектами в функциях типа `map`, то ему нужно отрывать руки с корнем, вне зависимости от языка.
Ну вот print — вроде грязная функция? А В Хаскеле можно передавать :)
Разумеется, нельзя, потому что в императивном языке эти две конструкции просто-напросто неэквивалентны.
f потенциально может выводить на экран, g — тоже, а порядок менять негоже.
Понятней не стало :-) Это бесконечный список из Null-ов или что?
Это просто список, т.е. либо пустой (Null), либо голова + хвост.
Значит его и фиг оптимизируешь толком
Ну как-то умудряются же, и чистота этому только способствует, потому что, например, map f . map g можно преобразовать в map (f . g) вне зависимости от того, что там за f и g.
Это самый обыкновенный список, и его определения достаточно, чтобы создавать бесконечные списки, лениво по ним бегать и т.п. Т.е. не нужно как-то отдельно писать итераторы, потому это легко использовать для своих типов данных.
Ну так и язык не функциональный, понятно, что сложные функциональные штуки типа каррирования и композиции функции в нём будут хотя бы чуть-чуть, но сложнее синтаксически :D
Чуть-чуть — это мягко сказано :)
Python по-умолчанию считает своих разработчиков достаточно вменяемыми
Да Python вообще динамический :) Так что Bondage & Discipline явно не в его стиле, конечно
Не очень понял, о какой функции, которая может ходить в разных направлениях, идёт речь, но опять же, не вижу причин, почему нельзя то же самое сделать на любом другом языке.
Речь не о том, можно или нет. В любом Тьюринг-полном можно много чего. Вопрос в том, чего это стоит.
-- | Дерево
data TreeT a t = Empty | Node a [t]
type Tree a = Fix (TreeT a)
-- | Бесконечное дерево, в узле n, поддеревья: genTree (n + 1) и genTree (n + 2)
genTree n = Fix (Node n $ map genTree [n + 1, n + 2])
-- | Свернём в список, первый элемент - узел, а затем идут свернутые поддеревья, которые обходятся в обратном порядке (т.е. сначала сворачивается последнее поддерево, затем предпоследнее, и т.п.)
foo = cata f (genTree 0) where
f Empty = []
f (Node x xs) = x : concat (reverse xs) -- reverse - это вот обратный порядок уже обойденных поддеревьев, мы лишь склеиваем "готовые" списки
-- | Берём первые 10 элементов
test = take 10 foo -- [0,2,4,6,8,10,12,14,16,18]
Вот здесь дерево ленивое — две строки.
foo — обходит поддеревья в обратном порядке
test — берёт только первые 10 от «бесконечного» дерева
В foo можно было не брать библиотечную cata (катаморфизм), а обходить ручками, если хочется более полный контроль, хотя помимо cata там есть и другие вещи, к которым и так сводится множество обходов и обработок.
> Да, забыл сказать, что map, filter и take возвращают те же ленивые итераторы, что и в хаскеле.
Вот только в хаскеле эти ленивые итераторы определяются так:
data List a = Null | Cons a (List a)
И так для любого своего типа данных.
> Да, g должна быть выводима во время компиляции
А так — нечестно :)
Я веду к тому, что необходимость «ленивых итераторов» очевидна, и везде так или иначе реализуется. Но в Хаскеле не нужно делать это как-то особенно, там оно делается по умолчанию. А чистота даёт больший просто для высокоуровневых оптимизаций.
Хотя, конечно, стоит отметить, что иногда излишняя ленивость становится источником проблем, когда неясно, где и в какой момент что-то вычисляется, а это может порой неслабо повлиять на поведение программы. Вот в этих случаях анализировать сложнее, но не так, чтобы очень.
Уж лучше идиоматично через list comprehensions, а то так и нечитаемее, и тормознее. А если идиоматично, то уже bar оттуда не вычленить так просто, композиционность страдает.
> Я ничего не знаю про Rewrite rules, но ни одна из перечисленных функций не обладает побочными эффектами.
Как же это не обладает, когда _может_ обладать (у меня там недаром f и g — произвольные функции), а это для оптимизатора весьма важно.
> По деревьям вы тоже итерируете последовательно?
Можно даже бесконечные деревья (как и списки) строить. Необязательно последовательно. Те же самые filter, map, что-либо ещё.
> Тогда добавьте интерфейс итератора к вашему объекту (переопределите некоторые специальные функции) и сможете итерировать по ним.
Итератор ходит вперёд. По дереву я могу своей функцией идти в том направлении, в котором мне надо. Это уже другой тип итератора совсем. Ходить вперёд — частый, хотя и распространённый, случай.
Понятно, что я не буду утверждать, что вот Haskell-way — самый лучший, я просто к тому, что дьявол в деталях. В придуманных примерах важнее одно, в реальных задачах — другое, причём в каждой что-то своё.
Я с D знаком весьма поверхностно, потому у меня есть ещё вопросы.
Если я верно помню, то! — это применение шаблона.
Значит ли, что g обязана быть известна в момент компиляции?
В коде, который я написал не вам, чуть ниже, но на основе моего:
foo = take n
bar = filter f
baz = map g
quux = foo . bar . baz
Проход по списку будет один. Верно ли будет это и в вашем примере, если аналогично дописать остальные функции, а потом их скомпоновать? Верно ли для обычного списка, или для только для итераторов?
> А вас какая часть этого выражения интересует? Функции работы со списками, каррирование или композиция функций?
Композиции и возможность записывать куски композиции отдельно, т.е. типа такого:
foo = take n
bar = filter f
baz = map g
quux = foo . bar . baz
> т.е. теми же самыми ленивыми списками
Да. При этом в чистом языке можно подряд идущие filter f . map g сворачивать в использование одной функции (Rewrite rules), потому что ни f, ни g не производят побочных эффектов.
А что если у нас не списки, а деревья? Как сделать всё то же, но для своего типа данных?
В общих чертах-то ясно. Плохой код должен выглядеть плохо. То есть идея в том, что глядя на вставку в середину массива, человек должен заподозрить неладное и убедиться, что оно к месту. Скажу за хаскель. Если бы писать там с мутабельными переменными было так же удобно, как без них, хаскель был бы другим. Вопрос только в том, что именно счесть плохим кодом, и вот тут да, мнения разнятся. Время покажет.
class List
class Null: List
class Node: List
Ибо ваше на хаскель переводится так:
data List a = List (Maybe a) (Maybe (List a))
Что совсем не то же самое
Отчего ж, есть и mapM и forM, но можно и так тоже.
Есть даже такое :)
Взять каждый (each) элемент, затем пойти внутрь конструктора Right (_Right) (при этом откинув те, что Left), затем взять каждый элемент (each), и затем первую букву (_head)
И для них выполнить putChar
Ну да ладно, это так, к слову.
Как я понимаю, суть в том, что вместо того, чтобы применить g, затем сделать, грубо говоря, yield, а затем ещё и f, можно сразу f.g применять. Насчёт чего-то ещё, не знаю, это уже более низкоуровнево. Но это был просто пример, суть которого в том, что даже в своей библиотеке можно задавать такие правила, заменяющие какие-то случаи на более эффективные реализации. Правда, тут всё на совести того, кто такие правила задаёт, хотя теоретически можно было бы ещё и требовать эквивалентности такой замены.
Вот, например, из документации:
К сожалению, глубоко тему не рыл, но вот, например: www.randomhacks.net/2007/02/10/map-fusion-and-haskell-performance
Стоит также отметить, что если в случае итераторов всё понятно, они специально так заделаны, то с двумя подряд foreach так уже сделать нельзя:
А если мы хотим, чтоб можно было — придется переезжать на итераторы, тогда как в хаскеле что foreach, что «итераторы» делаются одной конструкцией.
Написал несколько сумбурно, но, надеюсь, донес мысль :)
Ну так разумеется, ведь мы же именно то, к чему приводят эта ленивость, чистота и развитая система типов, и обсуждаем.
Я просто пытался донести мысль, что эти вещи нельзя мысленно просто перенести на другой язык и посмотреть, потому что они дают больший эффект, когда язык под них заточен.
Согласен, приятно было пообщаться.
Это не «стиль», это пример, показывающий, что в императивном языке такая замена неэквивалентна, а потому она не является оптимизацией, она является деталью реализации, о которой программисты должны знать, чтобы не совать в map грязные функции.
Ну вот print — вроде грязная функция? А В Хаскеле можно передавать :)
Куда уж конкретнее? Параметрический полиморфизм
Я специально написал «порядок». Если же map может применять функцию и в обратном порядке, то это уже не оптимизация.
Что выведет
test1()? Что выведетtest2()? А что выведетtest3()?Полиморфный
Разумеется, нельзя, потому что в императивном языке эти две конструкции просто-напросто неэквивалентны.
f потенциально может выводить на экран, g — тоже, а порядок менять негоже.
Это просто список, т.е. либо пустой (Null), либо голова + хвост.
Ну как-то умудряются же, и чистота этому только способствует, потому что, например,
map f . map gможно преобразовать вmap (f . g)вне зависимости от того, что там за f и g.Это самый обыкновенный список, и его определения достаточно, чтобы создавать бесконечные списки, лениво по ним бегать и т.п. Т.е. не нужно как-то отдельно писать итераторы, потому это легко использовать для своих типов данных.
Потому что в моём коде это не так.
Чуть-чуть — это мягко сказано :)
Да Python вообще динамический :) Так что Bondage & Discipline явно не в его стиле, конечно
Речь не о том, можно или нет. В любом Тьюринг-полном можно много чего. Вопрос в том, чего это стоит.
Вот здесь дерево ленивое — две строки.
foo — обходит поддеревья в обратном порядке
test — берёт только первые 10 от «бесконечного» дерева
В foo можно было не брать библиотечную cata (катаморфизм), а обходить ручками, если хочется более полный контроль, хотя помимо cata там есть и другие вещи, к которым и так сводится множество обходов и обработок.
> Да, забыл сказать, что map, filter и take возвращают те же ленивые итераторы, что и в хаскеле.
Вот только в хаскеле эти ленивые итераторы определяются так:
И так для любого своего типа данных.
> Да, g должна быть выводима во время компиляции
А так — нечестно :)
Я веду к тому, что необходимость «ленивых итераторов» очевидна, и везде так или иначе реализуется. Но в Хаскеле не нужно делать это как-то особенно, там оно делается по умолчанию. А чистота даёт больший просто для высокоуровневых оптимизаций.
Хотя, конечно, стоит отметить, что иногда излишняя ленивость становится источником проблем, когда неясно, где и в какой момент что-то вычисляется, а это может порой неслабо повлиять на поведение программы. Вот в этих случаях анализировать сложнее, но не так, чтобы очень.
> Я ничего не знаю про Rewrite rules, но ни одна из перечисленных функций не обладает побочными эффектами.
Как же это не обладает, когда _может_ обладать (у меня там недаром f и g — произвольные функции), а это для оптимизатора весьма важно.
> По деревьям вы тоже итерируете последовательно?
Можно даже бесконечные деревья (как и списки) строить. Необязательно последовательно. Те же самые filter, map, что-либо ещё.
> Тогда добавьте интерфейс итератора к вашему объекту (переопределите некоторые специальные функции) и сможете итерировать по ним.
Итератор ходит вперёд. По дереву я могу своей функцией идти в том направлении, в котором мне надо. Это уже другой тип итератора совсем. Ходить вперёд — частый, хотя и распространённый, случай.
Понятно, что я не буду утверждать, что вот Haskell-way — самый лучший, я просто к тому, что дьявол в деталях. В придуманных примерах важнее одно, в реальных задачах — другое, причём в каждой что-то своё.
Если я верно помню, то! — это применение шаблона.
Значит ли, что g обязана быть известна в момент компиляции?
В коде, который я написал не вам, чуть ниже, но на основе моего:
Проход по списку будет один. Верно ли будет это и в вашем примере, если аналогично дописать остальные функции, а потом их скомпоновать? Верно ли для обычного списка, или для только для итераторов?
Композиции и возможность записывать куски композиции отдельно, т.е. типа такого:
> т.е. теми же самыми ленивыми списками
Да. При этом в чистом языке можно подряд идущие
filter f . map gсворачивать в использование одной функции (Rewrite rules), потому что ни f, ни g не производят побочных эффектов.А что если у нас не списки, а деревья? Как сделать всё то же, но для своего типа данных?