Pull to refresh

Comments 35

При всём уважении к ФЯП, написание чистых функций — не самая интересная (читай: сложная) вещь. Вот грамотное сосуществование с сайд-эффектами от IO — это реальная проблема. И по моим ощущениям решения на ФЯП не всегда интуитивно и разумно отражают реальность.

Ближайшее место, где начинается веселье — база данных с конкурентным доступом. Все виды ORM, которые я видел, на Хаскеле выглядели так же трагично, как и на других языках. Начинают красиво, а дальше квирк на хаке и воркэраундом погоняет.
Не буду холиварить, замечу только, что F# функциональный и при этом никто не жаловался на работу с БД в нем. Впрочем, я согласен, что не все и не на всех языках получается хорошо и удобно. Я думаю, у меня будет статья и про вещи более приземленные, чем решение задачек.
Никто не жаловался ни на одном языке — потому что всюду фигачат как много.

Знакомство с фяп научило меня видеть чистые функции там где раньше была борода из классов и сайд эффектов. Но работать все-таки приходится на ооп.

А можно примерчик? Ну вот я скажем не понимаю, что является хаком в slick, хотя сейчас вижу его практически впервые.

Не хочу сказать, что это выглядит как-то там особо прекрасно, но и трагичности в этом тоже не наблюдается. Да, это делается не так, как привычно, иногда даже совсем не так. Но привычка — это все же немного другое, согласитесь. Это даже называется FRM, а не ORM, потому что маппинг тут не в объекты, а с функциями. Но опять же — это как раз естественно для функционального языка.
где мне нужно было срезать еще 10 символов, чтобы получить самое короткое решение

Серьезно? Ну я догадываюсь, что вы имели в виду заменить concatMap на монадический бинт. Но там и остальное можно неплохо ужать:
months = cycle [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

dude m d n = sum . take n . drop (d-1) $ flip take [1,3..] =<< drop (m-1) months

main = print $ dude 12 31 10

Вместо flip take [1,3..] можно же написать


(`take`[1,3..])

Будет еще короче

Давайте посчитаем:


(`take`[1,3..])``` -- 15 символов
flip take [1,3..] -- 15 символов

Хм, а значимые пробелы что, не считаются?..

Эта статья не про самое короткое решение, а про решение вообще. В конце написано об этом. Ужать можно еще сильнее.

В целом получилось неплохо, спасибо.

Только если уж вы пишете для начинающих, то советы типа «залезть в prelude и найти там нечто» — они не очень хорошие. Ибо откуда начинающий догадается, что ему нужно? Даже в случае суммы это далеко не так очевидно, как вы показываете. Не говоря уже про применение map, которое вы подаете так, как будто всем это давно известно. На самом деле, тем кому это очевидно, уже не нужно введение такого уровня — им захочется посложнее.

Ну и так, по мелочи — хорошо бы через проверку правописания пропустить, есть некоторое немаленькое число опечаток.

Так вот в статье как раз и написано где нужно смотреть и дана ссылка. Теперь начинающий знает, что есть такое место и что там можно что-то найти.
А что не известного в map? В питоне и яваскрипте он есть. Большинство людей с ним знакомы. Я не ставлю себе целью научить людей программировать. Статья для тех кто уже умеет, но боится хаскелля.
В продолжении будет посложнее, придется рассказать про типы и слово на букву м. Для одной статьи было бы слишком много.
Прошу прощения. Видимо, хромовый спеллчекер не очень. Сейчас поищу какой-нибудь сервис и проверю.

Не, ну ничего страшного конечно нет, в том числе и с map, просто можно было пару слов сказать, что это такой же точно map, какой вы возможно видели там-то. Просто какой-то переход от известных вещей должен быть, а тут местами у вас его не наблюдается.

Спасибо за замечание. Я часто мучаюсь из за того, что не могу понять, что нужно рассказать. Что очевидно, а что требует пояснения.

На самом деле одно нужное пояснение уже ниже написали — про ленивость. Я бы сказал, что оно самое существенное — потому что те языки, с которыми люди знакомы, в большинстве все-таки не ленивые. А это очень важно.

И не только в этом месте — ленивость в общем виде например позволяет написать if в виде функции с тремя аргументами, при этом не вычисляя аргумент для else до тех пор, пока он не потребуется. А если у else есть побочные эффекты, то вычислять его всегда — это моветон. Ну вы поняли, я думаю.

Побочные эффекты в чистых функциях — моветон независимо от ленивости или неленивости

Речь вообще-то не о том, хороши побочные эффекты или плохи.

Речь о том, что ленивость это как раз то, что сильно отличает хаскель от большинства других языков, где есть скажем такие же на первый взгляд map, fold и списки, потому что большинство из этих других языков все-таки не ленивые.

И на самом деле, если смоделировать поведение неленивого языка в хаскеле можно, и достаточно несложно, то чтобы смоделировать ленивость в изначально неленивом языке, придется довольно сильно напрягаться (и не всегда выйдет). Скажем, в java это стало делать сравнительно легко, когда появились лямбды. А если нет функций как 1st class object, то и вообще вряд ли получится.

И именно ленивость дает возможность использовать бесконечные структуры данных.
Статья для тех кто уже умеет, но боится хаскелля.
Я боюсь не haskell, а cabal. От того вместо haskell играюсь с clojure.
Где же самое короткое решение, спросите вы? Там такое дело, в общем, есть слово на букву м.
Хорошая статья, пишите про букву м. :)
Поясните, пожалуйста, эту магию
to_days max_days = [1..max_days]

Как ни прочитаю любую статью про хаскель, всегда долго медитирую на какую-то магию и вспоминаю вот эту историю.

А что тут, собственно, магического?


to_days max_days — это заголовок функции, говорящий что у нее есть 1 аргумент и задающий имя этого аргумента.
[1..max_days] это конструктор списка последовательных значений


Прямые аналоги:


function to_days(max_days) {
  return _.range(1, max_days+1);
}

def to_days(max_days):
    return xrange(1, max_days+1)

static IEnumerable<int> ToDays(int max_days) => Enumerable.Range(1, max_days);

Функция to_days с аргументом max_days которая при помощи синтаксического сахара [1..max_days] создает список от 1 до max_days. Ну т.е. [1..10] == [1, 2, 3, 4, 5, 6, 7, 8, 8, 10].
Gryphon88 сорри, не в ту ветку ответил

понял, спасибо. Просто смутился, что по разные стороны знака равенства одна переменная да ещё в разной семантике

Ничего страшного. Спрашивайте еще; мне нужно знать, что я не понятно объясняю.

Можете мне ещё объяснить пафос map? Читать код компилятора страшно, можете пояснить, почему map отработает только часть бесконечного списка? Если бы список был конечным, в чём отличие map от простого итерирования по списку?
В общем, нет никакого пафоса. И никаких отличий от простого итерирования по конечному списку нет. Просто в хаскелле(как и во многих других ФЯП) нет циклов как конструкций, все итерирование работает через рекурсию. А так как выполнение функций ленивое — итераций происходит ровно столько сколько нужно для получения результата и происходит это в самый последний момент при вводе/выводе (что, иногда, вызывает немалые проблемы). Когда мы ищем сумму то конечным условием для функции sum является достижение конца списка. Если искать сумму бесконечного списка то конечное условие никогда не будет достигнуто и все поломается. Для функций же вроде take или drop конечным условием является перебор заданного количества элементов, поэтому им все-равно где заканчивается список, они "проходят" только n заданных шагов, а значит map который перед ними выполняет только эти n шагов.

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

Что значит "в разной семантике"?

меня смутило, что слева max_days — список, а справа int. Неправильно понял синтаксис.
Есть еще замечательный оператор $ который как бы говорит хаскеллю: «сначала выполни все, что правее меня, а результат подставь на мое место».

По моему такое определение отражает суть оператора $ с искажением. В противном случае, в выражении
take 3 . reverse . filter even $ [1..10]
логично было бы убрать $, однако компилятор в таком случае выдаст ошибку:
take 3 . reverse . filter even [1..10]
    -- take 3 . reverse . [2,4,6,8,10]
    -- ошибка: применение композиции (.) к списку

Получается что оператор $ в данном примере говорит хаскеллю: «сначала выполни все, что левее меня, а результат подставь на мое место». А версия со скобками выглядит так:
(take 3 . reverse . filter even) [1..10]

Даже в одной из книг, помню, было определение типа «сначала выполни все что справа, потом примени». Со временем появились сомнение, и выяснилось, что '$' такой же оператор применения как ' ' (пробел), только первый имеет самый низкий приоритет — 0, а второй — самый высокий — 10.
К слову, оператор композиции (.) имеет приоритет 9, что объясняет ошибку компилятора в примере выше.

Спасибо за статью, с удовольствием прочитаю продолжение!

Говорить "сначала выполни все, что левее меня" — тоже ошибка, потому что приоритет "работает" в обоих направлениях: foo $ bar baz будет вычислено как foo (bar baz)


А в книгах пишут "сначала правее" подразумевая, что оператор $ — правоассоциативный. Иными словами, foo $ bar $ baz эквивалентно foo (bar baz), а вовсе не (foo bar) baz. В этом заключается его еще одно отличие от левоассоциативного пробела.

Говорить «сначала выполни все, что левее меня» — тоже ошибка, потому что приоритет «работает» в обоих направлениях:
Совершенно верно. Это был лишь пример другого «искажения» противоречащего с первым.

в книгах пишут «сначала правее» подразумевая, что оператор $ — правоассоциативный.
Отличие между оператором $ и обычным применением, конечно, не только в приоритете, но и в ассоциативности (в предыдущем комментарии я поторопился и пропустил это). Однако ассоциативность оператора с самым низким приоритетом может быть задействована, после того, как всё остальное в выражении приведено в нормальную форму. В этом и заключается «искажение»: вербальное подчеркивание именно ассоциативности по неволе затмевает важность приоритета вычисления. А это, в свою очередь, приводит к недопониманию в процессе изучения, о чем я собственно и написал.
Sign up to leave a comment.

Articles