Комментарии 267
А каким образом Хаскелю удаётся транслировать математические абстракции в машинный код? Например, я не понимаю, как учитывается переполнение стека при рекурсии. Любое применение функции — это Just x | bottom
. И я не понимаю, как с этим жить.
Фактически, это встроенный в язык паттерн «трамплин».
Конечно, компилятор делает и оптимизации хвостовых вызовов.
Вот паттерны − это специфическая вещь. Начиная с того, что книжка Банды Четырёх − это продукт борьбы (как минимум троих) авторов с особенностями и недостатками языка Java. Соответственно, понятна она только в этом контексте.
Видимо в функциональных языках нет паттернов просто потому, что они не нужны для сниппетов на конференцию по функторам и монадам.
Какие преимущества даёт абстрактная алгебра для реального программирования всё ещё никто не продемонстрировал.
Паттерны работают на более высоком уровне, помогая организовать код в структуру системы. Переименование интерфейсов в монады не сильно поможет выстроить структуру приложения, если только это не приложение с простым data-flow, которое можно вообще описать графической блок-схемой даже без типов.
Ну и где помогает-то, есть какие-то примеры из жизни? А то что-то все туториалы по монадам начинаются и заканчиваются матаном. Наверное полезно тем, у кого стоит задача программировать что-то из матана, да.
eDSL и детерминированные stateful вычисления не пишут без монад? ИМХО там гораздо проще допустить какую-нибудь логическую ошибку с монадами из-за лишнего синтаксического шума.
Пример из жизни по паттернам ООП. Есть что-то подобное про монады?
А в чем недетерминированность параллелизма с мьютексами и семафорами, например?
Инкапсулировать в функцию — это имеется ввиду возвращать функцию-исполнитель из функции-конструктора, возвращающей ту или иную функцию в зависимости от контекста? Да, неплохая идея! А если нам нужно несколько функций, то можно их вернуть в виде списка «ключ-значение». И функцию-конструктор, чтобы было понятно, назвать WidgetConstructor или WidgetFactory… Постойте-ка, да мы же изобрели ООП!
Что подразумевается под «гарантией»? Формальная верификация?
А как мы будем задавать тип возвращаемого виджета в зависимости от контекста? Тогда придётся пробрасывать инфу о контексте в функции-клиенты, что не есть хорошо.
Зачем это называть фабрикой?
Можете предложить более удачное название?
Любое другое незанятое
Например, какое, и чем оно будет лучше-то?
Тех кто устраивает фабрику ради «вынесения кода в абстрактный класс» (или отдельную функцию) надо бить линейкой по голове, пока не протрезвеют.
Фабричный метод — это не фабрика, это уже паттерн «Строитель» (Builder) — декомпозиция сложного конструктора.
Не надо путать. Это разные вещи.
Есть абстрактный класс компонента и тьма тьмущая его наследников.
На вход поступает некий конфиг, допустим JSON, по данным этого JSONа однозначным образом создаётся какой-то компонент.
Вот функция, которая реализует логику выбора подходящего класса наследника компонента, создаёт и возвращает экземпляр оного класса — получила гордое название ComponentFactory )))
В рантайме выбирать ничего из этой пары сотен не надо было.
Вам — не надо было, а нам — надо.
С сервера приходит инструкция (ага, JSONом) «Нарисуй-ка компонент с таким именем и такими параметрами». Клиент, соответственно, проверяет, «какой-там у меня класс лежит в мапе по этому имени?..»
А зачем тут какая-то фабрика?
Название такое.
А зачем тут какая-то фабрика?
В вашем примере — не нужна, но если вам надо будет написать какую-то ф-ю, которая сама не знает, какой объект ей надо создать (Foo или Bar), а решается это на call site — то у вас появится фабрика.
Это способ реализовать фабрику на функциональном языке.
функция на основании параметров решает какого класса компонент создавать.
Ну вот если она не решает, а решает внешняя ф-я — то это и будет фабрика. Сама ф-я не должна быть в курсе, какой объект она создает.
Ровно за тем же самым, за чем нужна любая high-rank полиморфная ф-я.
Допустим, у вас есть ф-я f, она вызывает ф-ю g. Ф-я g во время своей работы должна сконструировать некоторый объект с интерфейсом interface (я подразумеваю сейчас интерфейс в бщем смысле, вне зависимости от деталей реализации — оопшные, тайпклассы, какие-то свои велосипеды — тут не важно). С-но, ф-я g знает интерфейс, но не знает какой конкретно это будет тип.
Мы могли бы, конечно, сконструировать просто конкретное значение в f и напрямую закинуть в g, но:
- возможно нам надо в g создать несколько объектов соответствующего типа и что-то с ними поделать. тогда придется внутри f все эти объекты создать и потом в g закинуть
- возможно при создании объектов нужны будут какие-то дополнительные параметры, за проброс которых в конструктор, опять же, будет отвечать в данном случае f
Мы, допустим, не хотим, чтобы эта логика была в f, мы как раз хотим, чтобы она была в g. Тогда вместо того, чтобы совать в g конкретное значение, мы суем туда сам конструктор.
Вот такой конструктор, который засовывается в некоторую g, и при этом g — знает, что он конструирует объекты определенного интерфейса, но не знает, какие конкретно — это и есть фабрика.
Тогда непонятно, почему f вызывает g, а не наоборот. Выглядит как какой-то кривой дизайн.
Потому что f нужен результат g, а не наоборот :)
Почему?
Ну по той же причине, по которой вы передаете фунарг в map, например. Хотите разделить логику. Можете, конечно, передавать map в ее аргумент, а не наоборот. Но зачем? :)
То есть, она и объекты создаёт, и результат ей нужен?
Нет, f объектов не создает. f сует в g фабрику, а g — уже создает. f возможно вообще не знает как именно надо объекты создавать (какие аргументы совать в конструктор), но знает какой именно тип объектов надо создавать. А g — знает как создавать, но не знает конкретный тип (только интерфейс).
Так с map как раз отличный пример фунаргов. Но вот примера такой наркоманской фабрики всё нет, увы.
Ну вот есть у вас карандаши, ручки и фломастеры, ими можно рисовать. Ф-я g — умеет рисовать, но при этом обобщенно, через интерфейс (рисует и фломастерами и карандашами и ручками, при этом не обращая внимания на то, что перед ней).
Вы в f берете коробку конкретных объектов (например, карандашей), суете в g и говорите, что нарисовать. Вот эта коробка — и есть фабрика, т.к. g может при помощи нее получить объект, который рисует нужным цветом (при этом f вообще может ничего не знать о рисовании и цветах). И потом возвращает вам в f рисунок, написанный нужным штрихом. Потом в f делаете с этим рисунком что вам угодно.
Мне сложно представить себе практическую нужность такой архитектуры.
Ну обычное разделение ответственности. Все так пишут, в том числе и на хаскеле постоянно :)
Ну так у меня там неявно третья функция, которая и есть фабрика
Это вы про какую неявную функцию?
Я всё ещё не понимаю обсуждаемой проблемы.
Никакой проблемы нет. Есть задача — делегировать модулю некую логику, которая требует создавать некоторый объект, не привязывая объект к конкретному типу. Встречаются такие задачи постоянно, ничего специального в них нет. Определенный класс решений этой задачи называется фабрикой.
Так в том и дело, что оно там необычное и неокончательное.
Что неокончательное, в каком смысле?
Та коробка, которую я беру в f и сую в g.
Так а где она, хоть и неявная?
Мне сложно представить себе практическую нужность такой архитектуры.
Я вспомнил ещё один пример из жизни.
Есть у меня компонент, который реализует визуальный интерфейс для разработки фильта (ну, переключалки для логических операций и и кнопки составления дерева из них, окошки для ввода констант, выбора переменных и т.д. и т.п.)
Он в ходе своей работы по мере того, как юзер натыкивает мышкой фильтр, создаёт вспомогательные компоненты (представляющие элементарные фильтры, мапа «имя: класс» которых пришла с сервера) — для этого у него есть фабрика компонентов (она создана заранее и была передана в конструктор компонента-фильтра).
Так вот, у нас в проекте два вида фильтров, чуть-чуть отличающихся логикой внутри своих вспомогательных компонентов, причём логика самого компонента не отличается.
Самым простым решением оказалось создать вторую фабрику, набить её мапу чуть-чуть другими компонентами, и использовать для представления разных фильтров один и тот же класс, но с разной фабрикой.
Я долго думал и, кажется понял. Фабрика это функция, возвращающая копроизведение
Конкретный морфизм откуда? И на основании чего происходит выбор? Учитывая каррирование, частичное применение, представимость любого хом множества в категории hask, изоморфизм между a и ()->a это всё одно и то же.
Как-то однажды знаменитый учитель Кх Ан вышел на прогулку с учеником Антоном. Надеясь разговорить учителя, Антон спросил: "Учитель, слыхал я, что объекты — очень хорошая штука — правда ли это?" Кх Ан посмотрел на ученика с жалостью в глазах и ответил: "Глупый ученик! Объекты — всего лишь замыкания для бедных."
Пристыженный Антон простился с учителем и вернулся в свою комнату, горя желанием как можно скорее изучить замыкания. Он внимательно прочитал все статьи из серии "Lambda: The Ultimate", и родственные им статьи, и написал небольшой интерпретатор Scheme с объектно-ориентированной системой, основанной на замыканиях. Он многому научился, и с нетерпением ждал случая сообщить учителю о своих успехах.
Во время следующей прогулки с Кх Аном, Антон, пытаясь произвести хорошее впечатление, сказал: "Учитель, я прилежно изучил этот вопрос, и понимаю теперь, что объекты — воистину замыкания для бедных." Кх Ан в ответ ударил Антона палкой и воскликнул: "Когда же ты чему-то научишься? Замыкания — это объекты для бедных!" В эту секунду Антон обрел просветление.
Какие преимущества даёт абстрактная алгебра для реального программирования всё ещё никто не продемонстрировал.
Ну я уже где-то приводил неплохой пример полезного математического рассуждения.
Вот есть в js генераторы. Синтаксис генераторов изоморфен синтаксису do-нотации для call/cc монады без reentrance. При этом мы знаем, что любая монада имеет каноническое выражение через call/cc — значит, мы сразу знаем, что можно использовать синтаксис генераторов для любой монады, которая применяет фунарг внутри fmap'а не более раза. Например — та же async. А вот с list — канонически не выйдет.
Вполне себе полезный вывод. Более того — он конструктивен, т.к. доставляет универсальный способ использования синтаксиса генераторов в монадическом контексте и указывает конкретный критерий, по которому легко определить, когда — можно, а когда — нет.
Чем больше я живу и работаю с другими людьми, тем больше понимаю, что ООП не понимают ни они, ни, тем более, я. Или у всех какое-то свое понимание.
Ну у паттернов по сравнению с монадами есть один очень важный плюс — каждый паттерн включает в себя четко очерченный круг задач, для решения которых он предназначен.
С-но, две математически идентичные конструкции, но примененные для решения разных задач, будут двумя разными паттернами.
На монады не надо смотреть как паттерны, на монады (и прочие тайпклассы) надо смотреть как на доказательство (Карри-Говард, ага) утверждения «мой тип умеет то-то и то-то».
Это, конечно, можно, но штука в том, что код обычно пишется не для того, чтобы что-то доказать, а чтобы решить какую-то задачу. Так что такой взгляд совершенно неконструктивен. Кроме того:
надо смотреть как на доказательство (Карри-Говард, ага) утверждения «мой тип умеет то-то и то-то»
Это паттерны доказано "умеют то-то и то-то" (с-но, в определении паттерна и написано, что он умеет). А вот если я скажу: "Х — монада", то что умеет Х? У меня нет об этом никакой информации. Вообще говоря — ничего не умеет.
А если стоит задача доказать? В чем же тут неконструктивность? Доказательство один из способов верификации решения. Или тестирование и охрана ассертами это тоже неконструктивно?
Кто-то пишет библиотеки и возится с О-большими, корректностью, верификацией и прочим "матаном", а кто-то их комбинирует, не задумываясь, а считая, что "это уже сделано".
Я очень часто вижу, как программисты пытаются обособить себя от своего основного ЯП. С одной стороны, через академизацию своего опыта, мол, мы не программисты, мы computer scientists, мы не изучаем языки, а создаём их. С другой стороны, через ремесленничество − best tool for the job и прочие максимы. Но на практике я ни разу не наблюдал, чтобы кому-то удалось превзойти то форматирующее влияние, которое оказывает на способ мышления его основной инструмент. Одни мыслят категориями статически типизированных языков в динамически типизированных, другие − наоборот. Третьи плодят миллионы классов, ну и т. д.
Гамма работал в IBM, занимался VisualAge, Eclipse и JDT. Видимо, оттуда мои ассоциации с Java. Хотя было это, конечно, добрый десяток лет спустя.
реализовывать, в состав паттерна включается пример кода на языке C++ (иногда
на Smalltalk), иллюстрирующего реализацию
Это цитата из перевода 2001 года. И да, слова Java в тексте книги нет вообще.
Влиссидис имел бэкграунд в C++, но, наверное, больше работал с джавой на момент написания книги.
Книга вышла раньше чем Java.
Угу, я тоже когда на первый в жизни собес шел, начитался на Хабре всяких умников что ООП это инкапсуляция, наследование и полиморфизм, а это ведь люди которые искренне считают что понимают его настолько чтобы других учить.
Просто в математике так просто подменять понятия не получится
Вот с-но от натягивания на глобус совиной жопы в виде "отсутствие значения — это эффект" или "множество значений — это эффект", люди потом монады и "не понимают" :)
Нет, отсутствие значений или множество значений — это не эффекты. Если только не приложить к сове очень значительное усилие :)
Чем не эффекты?
Ничем не эффекты. Хотя, конечно, вы всегда можете растянуть сову до такого размера, что сказать "Х — эффект" будет то же самое, что ничего не сказать. Тогда тот факт, что вы называете списки или ИО эффектами, становится бессодержателен. То есть назвать что-то эффектом тогда — то же самое, что никак не назвать.
Бедная сова. Дело в том, что кто-то определился с определениями, в частности, понятия "эффект" и работает с ним, понимая что и зачем делает. А кто-то спорит о словах и применяет "совиную" аргументацию.
Замените "эффект" на "контекст", может быть, это поможет понять, зачем это нужно. Но главное — если вам монады не нужны, не ссорьтесь, а просто не используйте их.
Дело в том, что кто-то определился с определениями
Ну давайте называть это не эффектом, а "тирьямпампацией" и никаких проблем же. Тогда каогда вы будете другим людям объяснять монады они поймут все гораздо лучше.
Когда вы выбираете в качестве именования некоторого явления уже существующее слово — надо чтобы по смыслу, интуитивно, в каком-то плане ваше явление этому слову соответствовало. Иначе у вас выходит сова.
Замените "эффект" на "контекст"
И лучше совсем не станет. Списки — это, очевидно, не контекст.
Смотрите, вот есть молоток. Им можно забить гвоздь, а можно забить человека до смерти. Несмотря на то, что и там и там — "забить", смысл происходящего совершенно разный, т.к. в первом случае молоток используется в качестве инструмента, а во втором — в качестве оружия. И нет какого-то разумного способа абстрагироваться и объединить два этих варианта использования. Аналогичная ситуация с монадами. нет никакого смысла говорить о "просто монаде" — т.к. это понятие с исчезающе малым содержанием. Нет и не может быть никакого объяснение, которое характеризует поведение монад "в общем". Это главное что надо понять. И объяснить потом человеку, не понимающему монады, чтобы он понял.
А с динамической типизацией теряется весь смысл статических гарантий управления эффектами.
В случае монад статически гарантии часто даются самой формой интерфейса. Например, если у вас есть ИО монада, то как вы бинды с ретурнами в этом ИО не переставляйте, а unsafe получить не получится. И не важно в данном случае, какая типизация, просто невозможно применением данных ф-й напортачить, by design.
Если мы уже находимся в императивном окружении (ООП в общем его предполагает), то зачем там нужно как-то имитировать/реализовывать монады?
Все что нужно — инкапсуляция, возможности для сайдэффектов в любой ф-ции с параметрами по ссылке, возможность создания последовательности вычислений (с их прерыванием или протаскиванием состояния или генерирования исключения и т.п.) — все что угодно, для чего используются монады в ФП можно реализовать в ООП на императивном языке идиоматично для него вообще не прибегая даже к такому понятию.
Или я не прав? :)
Суть в том, что в императивных языках привнесение их в программу это не удел монад, в этих языках имеются куда более естественные и идиоматические средства их создания и паттерны проектирования. Поэтому когда в контексте императивного ООП языка упоминают про монады, то это по-моему только лишь для красного словца.
Кстати стоит упомянуть, что самый главный эффект, который достигается с любой из перечисленных вами монад — задание последовательности вычислений (например функция может что-то вычислить, залогировать это, и потом еще что-то довычислить, залогировать и вернуть результат) в императивных языках имеется из коробки (этот привычный эффект и поэтому всегда пропускается, но в ФП его можно добиться только зависимостью по данным).
И в Хаскеле программирование с эффектами тоже не удел лишь монад, для этого можно использовать и аппликативы (или вообще использовать какую-то свою шайтан-конструкцию). Но я уверен, что при желании можно и аппликатив начать демонстрировать как он выглядит и объяснять что это такое например на С++ или Питоне, просто не нашлось ещё желающих просветить программистскую общественность на это счёт. :)
Прелесть в том, что если она в этих монадах не живёт, то логи она точно не пишет и состояние не ковыряет.
Ну никаких гарантий же у вас нет на самом деле. Точнее, они ровно того же уровня, что в логи не будет писать ф-я, у которой в имени нету log, то есть "мамой клянусь".
Есть, компилятор не пропустит.
Пропустит. В хаскеле типизация эффекты контролировать не позволяет, она слишком слабая — берите unsafePerformIO и ломайте хаскель полностью сколько угодно, компилятор вам ни слова не скажет.
Кроме того, если даже будет позволять, то никакая типизация вам не сможет гарантировать, что вы не станете использовать тот же эффект в другой монаде, либо напрямую — вовсе без монады (монада это просто обертка вы можете ту же самую последовательность аппликаций написать и без монад, в лоб).
Типизация может гарантировать лишь то, что вы примените эффекты корректно.
На самом деле-то смысл монад не в том, чтобы "запретить и не пущать" — это все в любом языке делается элементарно и точно так же как в хаскеле — за счет инкапсуляции (типы тут никак не используются, мы просто небезопасные вещи приватим, а в публичный интерфейс выделяем только то, что не может сломать). Смысл монад (применительно к ИО) в том, чтобы интерфейс был безопасен, но при этом (вот тут главное) — достаточно выразителен. Безопасных интерфейсов мы можем стопицот мильонов понаписать.Только пользоваться подавляющим большинством из них будет, мягко говоря, неудобно.
Включу Safe Haskell, и у меня уже никто ничего не сломает. Срсли, почитайте ссылку, там клёво.
Ну потому что просто вы не можете использовать unsafePerformIO. Типы тут при чем?
Безопасность на типах — это когда у вас есть unafePerformIO но при этом вы не можете написать с ним некорректный код.
Мне вот в исходном комментарии очень хотелось написать, что, конечно, вы можете ту же State развернуть типах функции в явном виде, но это всё равно видно в типах функции. И -> (a, LogTy) тоже видно.
Ну так в итоге нету у вас никаких гарантий-то.
Ну хорошо, сделайте мне на C++ или JavaScript гарантии, что данная функция не пишет в файл и не посылает ничего по сеточке.
Делается в точности так же как в хаскеле — то есть просто убираете из языка все ф-и, которые пишут в файл и отсылают данные по сеточке, а вместо них добавляете ф-и, которые будут возвращать вам IO. В итоге написать программу, которая что-то куда-то пишет — просто невозможно, вне зависимости от типов.
Еще раз — если у вас просто нету ф-й, которые могут напортачить, то типы тут не при чем, это просто "мамой клянусь" разработчика языка. Вот добавил разработчик вам unsafePerformIO — и ваши типы ничем помочь уже не могут, с-но гарантий никаких не дают.
Гарантии уровня типов — это когда вы можете написать некорректный код (с тем же unsafePerformIO или с записью в файл), но компилятор вам выдаст ошибку. Когда вы просто не можете написать соответствующий код в принципе, то при чем тут типы? Если у вас нету ф-и, которая пишет в файл, то вы не сможете писать в файл ни на хаскеле ни на js.
Какое-то очень произвольное предположение. unsafePerformIO (как и assert_total какой-нибудь в более других языках) — это именно что способ нарушить типобезопасность
Но unsafePerformIO не нарушает типобезопасность. У нее вполне корректный тип. Интерпретатор прекрасно его чекает. Он вполне осмысленный, работает как надо. Все в порядке.
Почему? Вот функция возвращает Int, значит, она точно не пишет в лог и не ковыряет файлы.
Почему? Я могу внутри вызвать unsafePerofmIO, и она напишет.
Так а кто это IO выполняет-то? Или у вас просто какой-то скрипт для интерпретатора получается?
Я же сказал — как в хаскеле. То есть да, это просто какой-то скрипт для интерпретатора.
То есть, по итогу — вся безаопансость ИО в хаскеле заключается в том что вам разработчик стандартной библиотеки мамой клянется — у него все ф-и "правильные", а неправильные ф-и вы написать просто не можете, потому что любая композиция правильных ф-й — тоже правильная ф-я. Как только вы добавляете возможность получить неправильную ф-ю (unsafePerformIO), так сразу и превращается все в тыкву и типы никак не помогают.
Все в точности так же как было бы в каком-нибудь питоне.
Нарушает гарантии, даваемые типами.
Если гарантии, даваемые типами, у вас нарушаются так, что тайпчекер этого не замечает — то это в точности и означает, что никаких гарантий кроме "мамой клянусь" типы вам в данном случае не дают.
Кстати о последнем. unsafeCoerce-то хоть тоже в безопасном подмнжожестве языка можно отвергать, или тайпчекер и там что-то обязан?
Тайпчекер никому ничего не обязан. Он просто либо дает те или иные гарантии, либо не дает. Если тайпчекер позволяет вам написать некорректный код — то он не дает соответствующих гарантий корректности. Вы сами считаете иначе?
А кто в питоне мне статически проверяет правильность композиции функций?
А при чем тут композиция ф-й? Мы, вроде, про ИО говорили, конкретно — про гарантии того, что у вас какая-то там ф-я не пишет в файл или в сеть. Вот конкретно эти гарантии питон дает вам ровно настолько же, насколько дает хаскель — если вы уберете из языка все ф-и, которые позволяют написать в файл/сеть, то вы не сможете писать в файл/сеть, очевидно. Если вы такие ф-и добавите — то любая ф-я сможет писать в файл/сеть совершенно неконтролируемым образом. Что в хаскеле, что в питоне, не важно.
Я включил Safe Haskell, и функция теперь напишет только сообщение об ошибке посредством тайпчекера.
И какую конкретно ошибку типов вам выводит при использовании unsafePerformIO в Safe Haskell?
Если у вас есть магические функции типа unsafePerformIO, тела которых тайпчекер по большому счёту не видит (все эти хаки с распаковкой State — это именно что хаки и торчащие в хаскель-код кишки компилятора/рантайма), то ему просто проверять нечего.
Так а зачем проверять тело unsafePerformIO? Проверять надо тип. И в хаскеле вы не можете написать для unsafePerformIO такой тип, чтобы гарантировать корректное использование этой ф-и. В итоге корректное использование гарантируется тем, что мы просто эту ф-ю убираем с глаз долой из сердца вон. Но точно так же ее можно убрать в любом другом языке, в том числе динамическом. И получить такой же силы гарантии, таким образом. По-этому гарантии ИО в хаскеле обеспечиваются не на уровне типизации.
Ну что тут непонятного?
И примерно по этим причинам Safe Haskell не даёт вам иметь FFI-функции, живущие не в IO.
Но не за счет типов. Точно так же вы можете не давать иметь ффи-функции живущие не в ИО в питоне, ведь так? Никто же вам не мешает.
Нет, в хаскеле не сможет, если не использовать магию.
Дык и в питоне не сможете, если не использовать магию. В чем разница-то?
в хаскеле магия даётся компилятором и легко отключается/выгрепывается, а в питоне весь язык — одна сплошная магия.
Вот, легко отключается/выгрепывается — это верно. Только типы тут не при чем. Еще раз, берем питон и делаем любое ИО через unsafePerormIO. И, ВНЕЗАПНО, в питоне все столь же легко будет отключаться и выгрепываться.
мы получим
Но это не ошибка типов, это ошибка того, что вы импортируете то, что импортировать нельзя. То есть это инкапсуляция. Именно инкапсуляцией гарантии ИО и обеспечиваются в хаскеле. Не типами. И вы можете точно такую же инкапсуляцию устроить в любом языке, в том числе в языке без типов (условном питоне).
Тайпчекеры же не просто типы проверяют, тайпчекеры проверяют, что терм соответствует типу. А у вас тут терма нет.
Как нет? Есть. unsafePerformIO — вот ваш терм. И система типов хаскеля не позволяет присвоить этому терму такой тип, что этот терм можно было использовать гарантированно.
Дело не в том что чекер не выдает ошибку типов когда.
Я его нигде не могу написать, потому что семантика unsafePerformIO несовместима с тем, что IO — unescapable-монада.
Так ради бога, напишите для unsafePerformIO какой-нибудь другой тип. Но так, чтобы можно было с ней работать, а не жопу какую-нибудь.
Я же не говорю, что оно обязательно должно работать с типом IO a -> a, нет, выбирайте любой какой хотите.
Но не получится ни с каким. В этом проблема.
Ну и полезные в жизни тайпчекеры заведомо консервативны, поэтому всегда, для любого тайпчекера
Да чего вас в сторону тайпчекера все время уносит? Мы же про монаду ИО говорит, а не про типы.
Давайте возьмем питон, уберем из него все ф-и которые пишут в сеть или в файлы, а вместо нхи будут ф-и, которые генерят лямбды, пишущие в сеть или в файл (то есть семантически пусть IO a = () -> a), причем лямбды будут врапнуты, с-но запустить через простой apply их будет нельзя, но будет специальная ф-я unsafePerformMagic, которая и сможет такие лямбды запускать. Пусть unsafePerformMagic по дефолту лежит где-то там и ее вроде как никто не использует, но у нас будет ф-я бинд, которая юзает ее внутри и клеит вычисления, а так же запускатор наших скриптов будет при запуске применять unsafePerformMagic к определенному в скрипте значению main. Назовем такой язык Pure Python.
И вот теперь, внимание — объясните мне, чем гарантии ИО хаскеля более гарантии, чем гарантии ИО для Pure Python?
Мы, кроме этого, убираем саму возможность её написать самому.
Ну ради бога, из Pure Python тоже убираем.
Но никто не мешает и давать.
Всмысле? Ну же убрали это из библиотеки, все.
В том, что в хаскеле магия сиротливо стоит в уголке и легко контролируема, а в питоне — нет.
Это так. Но, еще раз — при чем тут типы? В Pure Python магия тоже в уголке и легко контролируема, но Pure Pyhton — динамический ЯП. Там нет типов.
А кто гарантирует, что в ИО через другие функции не будет?
Разработчик языка, как и в хаскеле.
Мне проще о модулях рассуждать в терминах типов (и вам, возможно, тоже, вы ж тапл читали).
Можно много о чем рассуждать в терминах много чего, но по факту система модулей в подавляющем большинстве языков с типами ничего общего не имеет, так что это неконструктивно.
Ну камон, терм — это то, что справа от знака равенства в лет-байндинге, а не слева.
Все константы являются термами. И, кстати, если y = f(x), то замена терма y на терм f(x) в каком-то выражении (офк, когда все ок со связыванием) не обязательно будет всегда корректна.
Ну и хорошо, потому что IO — unescapable-монада, и такого типа в общем случае и нет.
Нет в хаскеле. Потому что система типов хаскеля — слабая.
ЧТД, система типов нас снова спасает!
Всмысле, как спасает? Еще раз — мы спокойно может писать некорректный код с unsafePerformIO и компилятор ничего с этим не делает. В том время как он должен зарезать некорректный код, оставляя корректный.
А кто сказал, что для unsafePerformIO вообще такой тип существует с той семантикой IO и unsafePerformIO, которую мы имеем сегодня?
Ну вот вы пишите бинд через unsafePerformIO и все прекрасно работает.
Еще раз — unsafePerformIO, семантически, это корректная ф-я. В ней нет ничего плохого, т.к. вы можете писать с ней корректный, осмысленный код.
Я не вижу проблемы. То, что недоказуемо безопасный терм (и, вернее, доказуемо опасный) нетипизируем — не проблема, а хорошее свойство системы типов, на мой взгляд.
Так терм доказуемо безопасный, об этом речь как раз.
Ну и потому, что гарантии рантайм-поведения мне так же важны, как само рантайм-поведение.
Да, но ИО тут при чем?
Тем, что в хаскеле я могу посмотреть на сигнатуру функции и сделать вывод, что её передача в unsafePerformMagic ни к каким IO-эффектам не приведёт.
Нет, не можете. Тайпчекер хаскеля не гарантирует вам, что внутри вашей ф-и с хорошим типом (без ИО), где-то внутри не вызывается unsafePerformIO. Знать тип для этой гарантии недостаточно. Более того — знание типа для этой гарантии не является и необходимым. Иными словами — гарантии ИО в хаскеле не зависят от знания вами типа ф-и.
Ну блин, есть же совершенно очевидный изоморфизм между сегодняшним питоном и вашим пурепитоном, это скучно.
Именно так.
У меня есть цель посмотреть на функцию и сказать, если там IO/логи/nullable-семантика, или нет.
Ну вот как вы в хаскеле это делаете? Видите ф-ю, смотрите на ее реализацию. Смотрите на реализации используемых в ней ф-й (и так далее рекурсивно). Если нигде не встречается unsafePerformIO — значит, все ок.
Как вы делаете это в pure python? Смотрите на ф-ю, смотрите ее реализацию. Смотрите реализации используемых ф-й. если нигде не встречается unsafePErformIO — значит, все ок.
Разница-то в чем?
И непонятно, при чем тут тип. Вам же код надо смотреть, а не тип.
Выше я описал, зачем там на самом деле типы
Ну вы написали, что якобы по типу можно понять, есть там IO или нет. Но по факту же это неправда, нельзя.
Так это не константа
Константа, конечно. С-но все "внешние байндинги" — это константы и есть. В частности, это константа может иметь значение, которое в самом языке вообще писаться не будет.
Ну так откажитесь уже от магии, наконец, и включите -XSafe.
Так сейф не режет некорректный код, она просто запрещает применение unsafePerformIO.
Это, понимаете, разница как между тайп-сейф доступом по индексу и полным запретом доступа по индексу. В первом случае у вас зависимые типы, а во втором — тот же условный питон, в котором просто нету доступа по индексу.
Это верно для любой функции, даже unsafeCoerce: a -> b
С unsafeCoerce не получится.
Да, бывают контексты, в которых она корректна, но и сложение числа со строкой бывает корректно (если строка пустая).
И что должно получиться при сложении числа со строкой?
Какой и где?
unsafePerformIO. В реализации бинда, например.
Гарантирует, если вы тайпчекер не обманываете магией.
Если вы не обманываете Pure Python, то он тоже гарантирует. Вы можете объяснить, в чем разница в гарантиях хаскеля и Pure Python?
Достаточно посмотреть на выведенную аннотацию модуля, safe он или не safe.
Отлично, в Pure Python все то же самое. В какой момент разница появляется?
Это внешнее по отношению к тайпчекеру.
Именно так. Важно лишь, чтобы у терма был корректный тип, который бы не ломал нам все.
Технически она запрещает импорт. Но это даже неважно: я включил -XSafe, и всё, с этого момента сигнатуры функций мне (и тайпчекеру) не врут. Вообще. Никогда.
Ну отлично, в Pure Python все будет то же самое — запрещаете импорт unsafePerformMagic и гарантированно никто из ИО никогда и никак не вылезет.
Прекрасная аналогия с завтипами. Почему даже в этих языках есть костыли, в чём-то похожие на unsafePerformIO?
Почему?
абсолютно семантически корректная программа
Да? Разве хаскель вам гарантирует, что на любой возможной реализации эта программа отработает с одним и тем же результатом?
Можно пример?
Ну реализация бинда для ИО-монады в грязных языках так делается же. bind f x = f $ unsafePerformIO x
Поскольку на выходе ИО одно, то все вызовы развернутся в одну цепочку и это гарантирует корректность (вы ваши World* сольете из начального в конечный через непрерывную последовательность операций).
В том, что обмануть хаскель я могу только через unsafePerformIO и подобные хаки, а обмануть Pure Python я могу как угодно, включая доступные мне IO-абстракции.
Эм, нет, обмануть Pure Python вы можете тоже только через unsafePerformMagic. Просто потому что никаких других способов запустить ИО у вас нет, какие абстракции вы там не пытайтесь накручивать.
Гарантии есть. Их предоставляет компилятор. Чтобы ему помочь нужно приложить некоторые усилия. Стоит расслабиться и ваша программа превращается в одно большое IO, в дымке которого растворяются все парадигмы. Чтобы этого не происходило нужно потрудиться и разобраться.
Гарантии есть. Их предоставляет компилятор.
Какие гарантии ИО предоставляет вам компилятор хаскеля, но при этом не предоставляет компилятор питона (допустим, я в питоне написал bind-io, return-io и переписал все библиотечные ф-и так, что они возвращают io вместо того, чтобы сразу отрабатывать)?
Допустим я разогнался и врезался на байке в стену на скорости 200 километров в час. Тогда, действительно, никаких гарантий производитель шлема не даёт.
Повторюсь, нужно сделать усилие, и не пихать IO туда где без него можно обойтись. Тогда по сигнатуре функции будет видно что она делает и что ничего другого она сделать не может.
Допустим я разогнался и врезался на байке в стену на скорости 200 километров в час. Тогда, действительно, никаких гарантий производитель шлема не даёт.
Если не разгоняться, ехать по правилам и все такое — то и в питоне ничего плохого не случится.
Это тогда не гарантии, а фигня какая-то, извините.
Прелесть монад (и управления эффектами с их помощью) не в том, что если у меня функция живёт в какой-нибудь MonadWriter LogTy или State Foo, то я знаю, что она может писать логи типа LogTy или ковырять состояние типа Foo. Прелесть в том, что если она в этих монадах не живёт, то логи она точно не пишет и состояние не ковыряет. Чистое ФП нужно для того, чтобы функциям ничего лишнего не разрешать, а монады в нём позволяют разрешать только то, что нужно.Да, я понял что вы имели ввиду когда говорили об ограничениях эффектов только теми местами, где они нужны.
К слову о шайтанах, вот.Интересно, спасибо.
За что, простите?
Представляете, а ведь мы так говорим не чтобы запутать, а наоборот, чтобы разобраться :) и ведь получается!
Впрочем, это я дразнюсь. Получается не всегда, если залезаешь в незнакомую область ничерта непонятно. Но если надо, то можно, и это круто!
(a -> b)
в (F a -> F b)
. Используя «однобокий правый» функтор K для перевода из морфизмов (a -> b)
в (a -> K b)
можно построить категорию Клейсли. Что получается при использовании «однобокого левого» функтора U из (a -> b)
в (U a -> b)
?:)
Если вы про void
, который в С-подобных языках, то это действительно то же самое, что и пустой кортеж. Т.е. то, что используется, если нам не важен результат:
void main(void) {..}
main :: IO ()
К сожалению, такая вот путаница с названиями.
()
— это тип, в котором есть ровно одно значение, называемое ()
. Например, можно написать return ()
или дажеx :: ()
x = ()
void
— это тип, в котором нет ни одного значения. Нельзя написать return void;
или void variable = void;
.В стандартной библиотеке хаскеля пустого типа нет; он есть в пакете
void
и называется Void
:)Если сравнивать пустой кортеж и `Void` в Haskell с тем, что обозначается словом `void` в С, то **сишный** `void` — аналог пустого кортежа в Haskell, но не аналог ненаселённого типа `Void`.
В книжке, на которую я ссылаюсь, это тоже есть: bartoszmilewski.com/2014/11/24/types-and-functions
Константная стрелка она из терминального объекта, который не воид а юнит.
В принципе, вся «наука» проектирования ПО (в технической части) — это про изобретение абстракций (наборов интерфейсов и их взаимодействия друг с другом). Интерфейсы бывают похуже и получше. Одна из метрик «хорошести» интерфейса — это «ортогональность», т.е. возможность при помощи малого количества простых методов выразить большое количество всяких сложных вещей (как из трёх координатных векторов можно всё трёхмерное пространство сделать). Другая / похожая метрика — composability, т.е. возможность комбинировать с большим количеством других интерфейсов множеством разных способов. Ну и есть метрика «универсальности» или «абстрактности» — насколько большое количество разных вещей могут имплементировать этот интерфейс.
Математики профессионально и целенаправленно занимаются изобретением абстракций и компоновкой из них других абстракций уже больше ста лет (если считать, например, от Гильберта; а до этого занимались тем же, но не так профессионально и целенаправленно). Они наизобретали много «хороших», т.е. «ортогональных», «композабельных» и «универсальных» абстракций — множества, функции, группы, категории… Так как эти вещи по построению абстрактны / универсальны, ничего удивительного нет в том, что множество сущностей в программировании имплементирует эти интерфейсы.
Программисты занимаются построением абстракций… ну, скажем, с 50х. Товарищи инженеры из «банды четырёх» обобщили кучу инженерного опыта за пару десятилетий и выписали десяток распространённых, «хороших» абстракций. Многие из этих абстракций являются велосипедами — математики их изобрели на полвека раньше.
Хаскель начинали разрабатывать скорее математики, чем инженеры, поэтому они выбрали понравившиеся им знакомые абстракции. В частности, монады — интерфейс, который доказал свою «ортогональность», «композабельность» и «универсальность» в математической практике и продолжает доказывать в инженерной.
(в математике бывают ещё более лучшие абстракции, и в узких кругах имеет место… гм… спор на тему, почему бы не выбрать их вместо монад, но это уже другая история).
Ну и см. «You might have invented monads» (есть перевод на хабре — habr.com/ru/post/96421).
Вот штука, говорящая что точки с запятой в некоей зоне будут вести себя как-то по-другому, и правило, говорящее — КАК ИМЕННО они себя будут вести, и есть монада.
Мне всегда интересно было, почему, например, Монаду нельзя описать как некую хрень с сайдэффектом (нечто, что, например, пишет инфу в базу, в поток) или как некую защитную обертку над другими типами (Maybe вообще шикарно соотвествует Nullable из какого-нибудь C#). Почему обязательно пытаться притягивать категории, функторы? Я понимаю, что высокая наука требует применения спецтерминов, чтобы доказать свою научность. Но инженерная практика работает то с чем-то более осязаемым...
И кстати, я случаем не пропустил в статье сказочное утверждение, что в хаскеле попав в монаду, из нее нельзя выйти?
Я думаю, в основном потому, что авторы хотят показать, откуда эта абстракция исторически возникла. С другой стороны, это не вредно: категории и функторы сами по себе являются полезными абстракциями, имеющими множество имплементаций.
Представьте, что вы рассказываете про паттерн «абстрактная фабрика» студенту, который ещё ни разу не сталкивался с проблемами, которые этот паттерн решает: студент будет удивляться, зачем такая сложная штука нужна. А вот если вы будете это рассказывать программисту, который такие проблемы решал во множестве, просто по какой-то причине не знал, что этот паттерн так называется — он скажет «а, понятно, ок».
Для близости к инженерной практике действительно стоит к каждой абстракции приводить примеры инженерных проблем, в которых она помогает.
> в хаскеле попав в монаду, из нее нельзя выйти?
Это не является универсальным свойством монад. Из монады IO «выйти» нельзя (и то, есть всякие unsafePerformIO, но это скорее костыли); из монады State «выйти» можно стандартной функцией runState.
Я думаю, в основном потому, что авторы хотят показать, откуда эта абстракция исторически возникла. С другой стороны, это не вредно: категории и функторы сами по себе являются полезными абстракциями, имеющими множество имплементаций.
У меня ни разу не возникло возражений при подобном описании монад в учебниках по Хаскелю, написаных скорее математиками для математиков, чем инженерами для инженеров (разве что только один раз видел исключение у Шевченко (https://www.ohaskell.guide)). Но когда в заголовке вижу "Монады с точки зрения программистов" ожидаю все-таки инженерный подход. Sorry.
Представьте, что вы рассказываете про паттерн «абстрактная фабрика» студенту, который ещё ни разу не сталкивался с проблемами, которые этот паттерн решает: студент будет удивляться, зачем такая сложная штука нужна.
Скажу больше — неоднократно это делал причем не только для студентов-программистов, но и для гуманитариев, решивших стать программистами. Там гораздо сложнее было объяснить, что такое переменная и зачем нужна функция, чем на паре практических примеров показать, сколько кода надо писать с фабрикой и без.
Для шарпщика проще объяснить через линк. Пусть у нас есть дженерик тип X[T] (например IEnumerable или Nullable или Task.
реализуем для всех типов экстеншен метод SelectMany. теперь мы можем писать:
var result =
from x in X() // внезапно query syntax — это аналог do натации в хаскеле
from y in Y()
select x + y;
а проблема в том что мы не можем абстрагировать этот код от типов IEnumerable/Task/Nullable. В хаскеле можно, поэтому там дин и тот же код работает и со списками и с асинхронными знаниями и тд. в шарпе для тасков у нас будет своя функция Sum(), а для нулаблов своя.
Мне всегда интересно было, почему, например, Монаду нельзя описать как некую хрень с сайдэффектом или как некую защитную обертку над другими типами.Почему — как раз можно, и это наиболее понятный и утилитарно-обоснованный способ.
Почему обязательно пытаться притягивать категории, функторы?Это просто хаскеллисты притягивают, поскольку многие концепции и подходы к структурированию задач в Хаскеле могут быть описаны в рамках теории категорий, т.е. математически. А это очень подкупает.
И кстати, я случаем не пропустил в статье сказочное утверждение, что в хаскеле попав в монаду, из нее нельзя выйти?Например из монады IO выйти нельзя — «распаковать» значение без доступного конструктора или спецфункции компилятора не получится.
После семи лет ежедневной и плотной работы в Wolfram Mathematica, мне её показалось мало и в моём инструментарии появился Lisp. Ещё через пять лет мне стало тесно и в нём, так я пришёл к Хаскелю, Axiom, Agda, поскольку в них математическая мысль формулируется точнее и строже. И всё это прекрасно сочетается в работе и не мешает писать для развлечения на JavaScript :) Или вы предлагаете отменить все прочие языки, оставив лишь те, что нравятся вам?
Не всё. Монады не про ввод/вывод, а про композицию в контексте. Одним из контекстов может быть "внешний мир", но их гораздо больше: неоднозначные вычисления, вероятностные вычисления, параллельность и корутины… При этом синтаксически одна программа может выаолняться в разных контекстах. Это как ортогональные криволинейные координаты: один раз выразил дифференциальные операторы в терминах чисел Ламе и получаешь один и тот же дифур, но в разных контестах.
Зачем выращивать рис, когда есть макароны? Зачем снимать кино, когда есть книги? Зачем переусложнять и говорить по-китайски, когда есть понятный и простой румынский? Ответ прост: потому что можно. Так развивается человечество.
github.com/George66/Textbook
П.С. В английской написано " the goal of understanding the processes that preserve mathematical structure" и я этого не могу понять однозначно.
Категория из одного объекта не обязана иметь только морфизм id .
Так это нигде и не утверждается… Если тот же тип Bool
взять как единственный объект и функции Bool -> Bool
как морфизмы, то их 4 штуки показано. Мне просто не хотелось для каждой стрелочку рисовать, чтобы рисунок не загромождать.
В вашем тексте есть утверждение, что функтор в категорию из одного объекта переводит все морфизмы в id. В общем случае это неверно.
Программа выводит на консоль «Напечатайте фразу»
И ждет вода пользователя
Пользователь вводит что-то
Программ после ввода просит повторить ввод
И если повторный ввод совпадает, программ в консоль выводит «Ok»
Если нет, то «Ошибка»
Тут явно нужно как-то хранить состояния ввода и делать сравнения
Можно так:
io1 = do
putStrLn "введите текст"
x <- getLine
putStrLn "повторите ввод"
y <- getLine
if x == y
then putStrLn "Ok"
else putStrLn "Ошибка"
Как видите, программа вполне соответствует тому, что она делает (хоть и монады).
Но если бизнес-логика становится сложнее, её лучше выделить в чистую функцию, а ввод-вывод скомпоновать отдельным блоком. Например, как-то так:
io2 = process
<$> (putStrLn "введите текст" >> getLine)
<*> (putStrLn "повторите ввод" >> getLine)
>>= putStrLn
where
process x y = if x == y then "Ok" else "Ошибка"
Для меня одним из декларируемых свойств функционального программирование — это то, что выполнение функции не зависит от состояния. А тут явная зависимость вывода функции getLine от состояния вычислительной машины. Она же может никогда не отработать. Функция всегда должна при одинаковых параметрах возвращать одинаковый результат. А тут getLine всегда получает пустой параметр, а возвращает разные результаты.
Для меня, вот эти две функции, выглядят как костыли в языке, Haskell. Которые возможны только по воле создателя языка. А вот если абстрагироваться от реализации? А оперировать абстрактной вычислительной машиной.
Или в общем-то мы все равно придем к глобальному хранению состояний машины? Или программа на функциональном языке — это всегда функция? В чистом виде, без костылей. И при вызове программы мы всегда должны передать ей входные параметры, а она при одинаковых входных параметрах всегда должна возвращать одинаковый результат.
Типа $ plus 1 1
2
И в чистом виде, она никогда не должна вернуть, например 3. Хотя с функцией getLine, такое сделать можно в легкую.
То есть вопрос не про Haskell, а про философию функционального программирования.
Можно подходить к этому так. Программа с IO это чистая функция — рецепт того, что делать при конкретных входах. После чего мы подаём ей на вход разные варианты внешнего мира так же, как в интерпретаторе функции plus подаём разные числа. При этом, концептуально, одинаковым мирам всегда соответствует одинаковый ответ. getLine, в мире, в котором клавиатура всегда возвращает "123", всегда вернёт "123". миров много, но и у функции plus возможно много входных параметров.
Не воспринимайте IO-функции как нечто инородное в чистом ФП, а монады требуются не для обеспечения чистоты, с этим проблем нет, а для обеспечения порядка вычислений в ленивом языке.
Абсолютно согласен. Мне хотелось показать, что ничего "магического", "неправильного" или чуждого идее ФП в функциях типа getLine нет. И вообще, разницу между императивным и функциональным подходом хотелось бы демистифицировать. Можно писать чистые функции на С, и даже выгоды от этого получать (в FORTRAN есть ключевое слово pure, помогающее компилятору трансляции чистых функций), но это акт доброй воли. Можно в Haskell запихать всё в IO, но компилятор из помощника превратится в послушного толмача. А можно балдеть от оптимизаций gcc и ghc, подавая им правильно приготовленные программы, расширяя горизонты. Кайф!
#include <stdio.h>
#define N 10000u
const char* compare(unsigned x, unsigned y)
{
static const char LT[] = "LT";
static const char EQ[] = "EQ";
static const char GT[] = "GT";
return (x < y ? LT : (x > y ? GT : EQ));
}
int main()
{
for (unsigned x = 1; x <= N; ++x)
for (unsigned y = 1; y <= N; ++y)
printf ("%u <%s> %u\n", x, compare(x ,y), y);
}
именно «отделив бизнес-логику в чистую функцию».Хороший пример, спасибо!
Идиоматичное решение здесь может быть таким: создадим чистую полиморфную функцию, которая соответствует вложенному циклу:
mkComps :: PrintfType a => Int -> [a]
mkComps n = [printf "%u <%s> %u\n" x (show (compare x y)) y
| x <- [0..n]
, y <- [0..n]]
где compare :: Ord a => a -> a -> Ordering
— это чистая библиотечная функция, соответствующая вашей.
Эту функцию можно вызвать в чистом контексте:
> concat (mkComps 3) :: String
"0 <EQ> 0\n0 <LT> 1\n0 <LT> 2\n0 <LT> 3\n1 <GT> 0\n1 <EQ> 1\n1 <LT> 2\n1 <LT> 3\n2 <GT> 0\n2 <GT> 1\n2 <EQ> 2\n2 <LT> 3\n3 <GT> 0\n3 <GT> 1\n3 <GT> 2\n3 <EQ> 3\n"
или в IO
main = sequence_ (mkComps 3)
> main
0 <EQ> 0
0 <LT> 1
0 <LT> 2
0 <LT> 3
1 <GT> 0
1 <EQ> 1
1 <LT> 2
1 <LT> 3
2 <GT> 0
2 <GT> 1
2 <EQ> 2
2 <LT> 3
3 <GT> 0
3 <GT> 1
3 <GT> 2
3 <EQ> 3
это был вывод в консоль.
Здесь mkComps
выполняет всю работу чистым образом, "производя" данные, а в main
они превращается в вывод на печать. Благодаря параметрическому полиморфизму функция printf
может возвращать как "чистую" строку, так и побочное действие. А благодаря ленивости языка никакого списка в памяти при этом не создаётся, хотя выглядит он подозрительно. На моём ноутбуке откомпилированный бинарник отправил в /dev/null
10000*10000 записей за 10 секунд.
mkOut :: Int -> [String]
mkOut n = [ shows x $
showString " <" $
shows (compare x y) $
showString "> " $
shows y "\n"
| x <- xy, y <- xy ]
where xy = [1..n]
main = mapM_ putStr $ mkOut 10000
И он меня очень не порадовал — память заканчивается, система начинает подвисать.Такая версия — сделать список из IO-действий — не является по сути отделением логики от операции вывода:
mkOut :: Int -> [IO ()]
mkOut n = [ putStr $
shows x $
showString " <" $
shows (compare x y) $
showString "> " $
shows y "\n"
| x <- xy, y <- xy ]
where xy = [1..n]
main = sequence_ $ mkOut 10000
Насколько я понял решение можно сделать таким, используя полиморфную функцию-префикс:
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
class Out t where out :: String -> t
instance Out String where out = id
instance Out (IO ()) where out = putStr
mkOut :: Out t => Int -> [t]
mkOut n = [ out $
shows x $
showString " <" $
shows (compare x y) $
showString "> " $
shows y "\n"
| x <- xy, y <- xy ]
where xy = [1..n]
test :: String
test = concat $ mkOut 10000
main :: IO ()
main = sequence_ $ (mkOut 10000 :: [IO ()])
(Какие-то нюансы не знаю, что мешают, чтобы не нужно было специфицировать тип результата mkOut
при вызове.)Примером декомпозиции в Haskell может быть реализация игры "Змейка" на RosettaCode: http://rosettacode.org/wiki/Snake#Haskell
Думаю, вполне правильно. Для управления состояниями строгие и императивные языки в монадах не нуждаются. Но монады и в этих языках позволяют организовывать поток вычислений с весьма изощрённой логикой. Когда-то я определил для себя, что монады позволяют мне перегрузить операторы :=
и ;
определив в них свою семантику. Это случилось во времена Лиспа — не чистого и строгого языка.
Грубовато вышло… вызывает желание ответить тем же.
Категории очень легко и естественно визуализируются как ориентированные графы. В принципе, любой ориентированный граф можно достроить до категории, добавив композиции морфизмов и тождественные морфизмы, если необходимо.«если необходимо»? То есть, можно назначать композиции уже имеющимся морфизмам? (Иногда можно) А вот попробуйте решить задачку, которую я сам себе в своё время придумал, а решая просветлился.
Постройте для произвольного графа категорию, где стрелки — всевозможные маршруты в данном графе без повторяющихся вершин, то есть почти свободная категория, но со стянутыми в id циклами, всё просто, riiight?
Внезапно, такие приколы есть и в «строгом» хаскелле, за примером далеко ходить не надо:
Существует целый класс типов, который так и называется, Functor.Эта фраза меня просто убивает, именно потому что так говорят все, а это неверно. Тип не может быть функтором, функтором может быть конструктор типа. [a] — не тип, тип — [Int]. Это дико мешает начинающему воспринять концепцию параметрических типов, ибо тех поначалу за одинаковым синтаксисом не замечаешь. В С++ тут будет шаблон и его, по крайней мере, сразу видно.
Тяжёлое наследие сортов. [a] это тип, просто сорт у него не , а ->*.
* Автор явно не Фейнман. На кого рассчитана статья не понятно.
* Отличный пример, что использовать монады можно понимая ничего в теоркате, а значит и углубляться в него нужно только если действительно хочется.
Монады может быть, но есть ещё профункторы, естественные преобразования, комонады,T-алгебры и коалгебры, монадические трансформеры, линзы…
Можно интегралы считать как площадь под графиком трафаретом, по клеточкам, и ныть что высшая математика это сложно и никому не нужно, потому что есть трафарет а самые частые случаи уже посчитали (это я про паттерны проектирования).
Чтобы мосты строить тоже снипы есть, но, почему-то, строители учат и математический анализ и механику и сопротивление
Оставьте суждения о математике математикам.
Я думал это я тугодум, но уже на магитсратуре софтинженерии — пришел на кафедру прикладников — там по каким-то дням решили публично передоказать что-то модное, прозвучавшее (уже не помню что) и раздавали доцентам по страничке подготовить перед студентами и профессурой (многодневное доказательство, странчек на 20-40, но в конкретный день — по 2-3 проходили). Это было мучительно. Каждое утверждение куда-то проваливалось и это было бесконечным углублением. И я понял почему передоказывают вообще «по кафедрам» так мало — ну никому не надо, не интересно, как использовать не ясно, а работа тяжелая. Так вот я вполне могу сказать верю я ваши бурбакизмы и быстрей вернуться к своим моделям.
урматы
Можно, для тех, у кого математика была только два курса, расшифровать эту аббревиатуру? Чтобы знать, что гуглить.
Т.е. это просто математический инструментарий физика. При этом физика формально не особо-то колышет почему оно «работает», главное — как и где это применять.
Ну, да, поэтому наш курс во многом выродился в «для уравнений такого-то вида надо искать решения в такой-то форме».
Да ладно, это даже часто для курса диффуров верно, чо уж там про урматы. Да и "ищем в нужной форме" это ладно еще (и хорошая, кстати, задачка — получить тот же самый результат, но естественным образом, без подгонки), вот "из естественных физических соображений бадумц!" — вот это да! :)
Ну это всё вопрос интересов, как вы в самых первых пяти словах и написали.
И тут приходят профессора, всех строят под свои интересы, утверждают, вон как свидетельствует Арнольд: «ноль это положительное число… и вообще теория множеств — язык математики». А если им аккуратно сказать «я так не думаю»… влепляют минус в карму.
concat :: [[a]] -> [a]
concat = join
Есть хороший критерий для самопроверки. Вы должны потерять способность объяснять что такое монады.
А я попробую объяснить "для программистов", по-проще.
И так, начальные условия. Мы что-то там начитались про функциональщину и теперь взяли строгий принцип: только чистые функции, только хардкор!
Берём чрезвычайно программистский язык С (не волнуйтесь, кода не будет). В том числе и потому что там можно всё писать чисто на чистых функциях. Ну там main получает указатель на массив строк аргументов и выходит с интовым кодом возврата (т.е. это (**char -> int) ), а вся грязь только от нас.
Отлично. Пока полёт нормальный. Хотим написать утилитку, которая берёт из аргумента имя и выводит на экран "Привет, имярек!" (чуть более сложный Hello, world!).
Тут у нас случается "упс". В **char -> int ничего нет про экран. Т.е. хотим грязного! Как работать с грязью? Правильно — абстрагироваться.
И так есть экран, чьё состояние изменится, но мы хотим оставить наши руки чистыми. Мы говорим себе — нет экран на самом деле не изменится. Точнее, что он там себе изменится внутри — нас, чистюль, не колышет, для нас он как был так и останется сам собой — экраном. Точнее, сохраняет свой тип (а конкретное значение меняется).
Какие бы мы преобразования над экраном не делали, они дадут нам экран. Другой экран уже с начерканными там словесами, но экран. А раз так, то мы — чисты, мы ничего не меняли! Это как 2+2=4 сложить. Только мы берём экран, берём операцию написания слов, применяем, получаем другой экран. С точки зрения нашего кода и нашего абстрагирования от экрана — никаких побочных эффектов.(Чтоб программа main как функция была не тривиальной ещё возвращаем из неё 1, если экран вычислился какой-то ошибочный, и 0 если нет)
Как не сложно догадаться такое понимание экрана — и есть монада.
Т.е. мы все грязные изменения состояния превращаем в абстракции над вычислениями (ой это уже сложно зашло).
С опшиналом так же как и с экраном. _Считаем_ внутренние состояния (в данном случае null — не null) просто за различные константные значения, изменения — операциями (блин, так и подмывает «функторы» и «морфизмы» писать, но я пока держусь).
Тем более там операции-то простые null с чем угодно даёт null, а что угодно с не null даёт туже операцию.
Моё описание имеет некоторую общность.
Вот монаду Promise так просто уже тяжелее будет объяснить в таких упрощённых терминах, потому и не буду.
Автору спасибо за попытку, но, к сожалению, все-таки не для программиста(= В отличие от этой статьи: после десятка подобных вашей наконец-то пришло понимание. А все потому, что ничего лишнего в объяснении — только код.
Первый пример — сильно обрезанный экземпляр монады Writer с единственной возможной функциональностью которую можно реализовать в данном случае — конкатенация вывода лог-сообщений. Третий пример (когда функции класса
Employee
могут возвращать None
вместо значения) можно интерпретировать как монаду Maybe. Но как было уже сказано: "Монада — это не maybe. Наоборот maybe — это монада"Во втором примере монады как таковой нет. Да, это effectful-вычисления, но ни в каком виде не монада. Что-то типа:
data Backtrace a = Backtrace {
getResult :: a,
getBacktrace :: [a]
}
class IdFunctor f where
($$) :: (a -> a) -> f a -> f a
infixr 0 $$
instance IdFunctor Backtrace where
($$) f (Backtrace x b) = Backtrace (f x) (x : b)
withBacktrace :: a -> Backtrace a
withBacktrace x = Backtrace x []
f1 x = x + 1
f2 x = x + 2
f3 x = x + 3
-- обычное вычисление
result1 = f3 $ f2 $ f1 $ 0
-- вычисление с обратной трассировкой
resultWithBacktrace = f3 $$ f2 $$ f1 $$ withBacktrace 0
result2 = getResult resultWithBacktrace
backtrace = getBacktrace resultWithBacktrace
Монады с точки зрения программистов (и немного теории категорий)