Pull to refresh
18
0
Dmitry Antonyuk @lomeo

User

Send message
Для данного примера ленивости достаточно. Cont хорош не сам по себе, а в трансформерах, когда нужно оборвать вычисление в какой-то монаде.
Как пример, можно почитать про подход Denotational design with type class morphisms.

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

В общем, в Haskell-е надо использовать моноиды-функторы-аппликативные функторы-монады-стрелки вовсю. И, говорят, будет счастье.
Мешает. Т.к. вы подтвердили, что это синтаксис Haskell, то я правильно распарсил ваше замечание «IEnumerable f => (a -> b) -> f a -> f b выполняется всегда».

Однако оно не выполняется: IEnumerable f говорит о том, что тип обязан реализовывать интерфейс IEnumerable, так? Причём таким образом, чтобы тип у Select был (a -> b) -> (f a -> f b). Однако это не так. Такой тип вообще невозможно описать на C# — он не поддерживает higher kinded types. Ср. с примером выше на Scala или функтором на Haskell.

Что я хочу подчеркнуть: в теоретико-категориальном смысле функтор — это отображение, сохраняющее структуру. Т.е. как минимум (=== — структурное равенство):

f.Select(x => x) === f

Если в C# это не так, то это не функтор.
> Я даже сам удивился, что там так мало Си, когда смотрел.

Ну так об этом и речь! В этих языках своя песочница, для связи с внешним миром им нужен натив. (Мы, кажется, уже в двух потоках дискутируем о тезисе «чистота мешает IO»)

> Во всех примерах message-queue, которые я видел на Haskell, клиент и сервер должны быть частью того же вычислительного процесса, в котором реализован этот канал.

Distributed Haskell. Куча библиотек с Erlang-like исполнением. Там же ничего умного не надо. Есть библиотеки сериализации, пишем/читаем из сокета. Да и вообще, практически любой обмен с сетью организуется через пару каналов в двух потоках. Ничем не отличается от обычного кода на императивном языке, ну может чаще TChan используют :)

> Но проблема в том, что нужно понимать и ограничения подхода, а он преподносится так, что никаких ограничений в нём нет, что он универсальный и лучше других по показателям: удобство, надёжность, выразительность, простота.

Уже то, что мы с вами ведём этот спор говорит о том, что в этом подходе есть ограничения ;-) И да — нет в мире совершенства.
Чтобы подчеркнуть — я спорю исключительно с замечанием о том, что чистота и ленивость языка как-то мешает, например, читать/писать из памяти.

> А канал связи — это система, у которой нужно явно менять состояния. Если мы рассматриваем сам Haskell (ну, или любой чистый функциональный язык), то там нет такой концепции.

Чистота не мешает наличию канала связи в языке. Ведь канал связи можно реализовать по разному. Необязательно менять его состояние. Например, можно последовательно читать и возвращать новые состояния. Можно реализовать монадами, как сделано сейчас. Можно использовать cps. Внешний мир в любом случае узнает об изменении канала связи через натив. Как и в Java, который не чистый. Хотя бы поэтому — понятия IO и чистоты ортогональны.

> В Haskell, конечно, есть и указатели, и ссылки, и IO, но их невозможно запрограммировать на самом Haskell, поэтому они создаются искусственно, через специальные монады связанные со внешними библиотеками на Си

«Через монады» — например, да, «связанные с внешними библиотеками на Си» — не обязательно. Если вы про теоретическую возможность («невозможно запрограммировать»), то IORef можно написать на State (Map Address Value), например. Натив не потребуется.
Sample — не интерфейс, а класс, реализующий IEnumerable.

Вот у вас написано: IEnumerable f => (a -> b) -> f a -> f b — это синтаксис Haskell? Если да, то что здесь будет в f, как пример?
Это разные A. Давайте назовём первый A, скажем, Sample. Тогда читать так: «для каждого класса Sample, имплементирующего интерфейс IEnumerable, метод Select не будет иметь return type Sample<TResult>, следовательно IEnumerable — не функтор».
> Указатели тут ни при чём.

А что причём? И почему — объясните. Про указатели я могу объяснить — как только мы добавили указатели, мы получили прямой доступ к памяти и значит можем в неё писать и читать из неё :-)

> И как только вы добавите указатели чистый язык превратится в грязный…

См. безопасная работа с указателями в Haskell. О некоторых вариантах я уже писал вам.
Это не проблема чистого или ленивого языка. Это проблема языка, у которого нет указателей. Добавим их — и вуаля — вдруг чистый и ленивый язык начал испукать пучки фотонов прямо пользователю в глаз :-)
Как настроить массив на видеопамять в Java, не используя натив? И да, если мы имеем Ptr, настроенный на видеопамять в Haskell, то тоже можем записывать и считывать в/из него данные.

Console.Read пускай нам возвращает одно и то же, чем это может мешать? Главное, чтобы из этого «одного и того же» мы могли брать разные значения по мере того, как их вводит пользователь, например. Сделать это можно монадами, потоками, продолжениями…

Аналогично с Console.Write.

Про примеры с клиент и сервером — не понял. Я писал такие приложения на Haskell, писал отдельно сервер на Haskell, клиент на Java, например, писал клиент на Haskell… Поэтому замечание про «клиент и сервер — это часть одной программы» совершенно не понимаю. Объясните, будьте добры.

> Нужны всякие костыли разной степени костыльности.

Согласен, но вы так говорите, как будто это что-то плохое :) В подходе чистый-ленивый есть плюсы и для этого — мы можем легко комбинировать действия, т.к. они первоклассны в языке.
Разве? Т.е. для каждого A: IEnumerable метод Select тоже будет возвращать A? Нет, поэтому IEnumerable — это не функтор. Сравните со Scala:

trait Functor[F[_]] {
    def fmap[A, B](r: F[A], f: A => B): F[B]
}
> потому что на чистом ленивом языке ввод/вывод просто даже теоретически нельзя запрограммировать.

Почему?
Дело не в том, функциональный язык или нет. На Java, например, нельзя написать вывод на консоль. Тоже нужно использовать нативные функции. Чем это мешает, не пойму — раскройте философскую подоплёку проблемы :)
Так всё не Haskell. Синус тоже, наверное, не на Haskell, или вывод на консоль. Важно где мы этим пользуемся.
poke (intPtrToPtr 0xb800) 0x00? Только это сломается.

В Haskell с указателями можно работать как напрямую (через Foreign.Ptr), так и безопасно (например, через IORef).

Для многих задач не нужны Foreign.Ptr и можно использовать что-то более безопасное. Например, для сравнения указателей можно использовать Foreign.StablePtr. Если нужно выделить память, поработать с ней напрямую, подсчитав что-то, то для этого есть безопасные механизмы Foreign.Marshall (см. Foreign.Marshal.Alloc, Foreign.Marshal.Utils).
Если в 5 задаче пометить lo как final, то hello = «Hel» + lo тоже вернёт true.
В ЖЖ: ru_lambda, ru_declarative
На c.j.r haskell@ :)
proc var всегда возвращает одно и то же значение типа IO () и не меняет состояния. Вот если ей ещё один параметр передать типа RealWorld… :) Так что всё таки функция.

(Надо было с unsafePerformIO показывать)
Основная идея — использование преимуществ табличного подхода, не спотыкаясь о его недостатки. Т.е. повышение скорости (которая теперь линейно зависит от количества переменных) вычисления функции при небольшом размере структуры данных.

Применяться могут везде, где нам необходимо быстро получить результат для небыстрой функции с небольшой областью применения (для примеров из википедии, состоящей из 2 элементов, а по сути вспомним, что у нас везде биты — мы ведь можем выразить функцию как набор булевых функций, например). Также BDD обладают свойством уникальности — для данной функции при данном порядке переменных существует единственный ROBDD. Поэтому может применяться для формальной верификации, проверки на эквивалентность.

Мне в этой структуре нравятся три вещи

1. Идея реализовать булеву функцию в виде графа, ускоряя вычисления.
2. Возможность производить вычисления не распаковывая BDD.
3. Уникальность.

И не нравится, что нахождение оптимального порядка переменных — NP полная задача (O(N!) кажется).

К сожалению, это оффтопик, поэтому здесь остановлюсь.

И спасибо за статью!
Что мне в нём понравилось — благодаря возможности переписывания операторов (особенности языка, конечно) мы можем записывать выражение естественным образом. У Вас это тоже есть. Однако, использовав ленивость by default мы бесплатно получаем целую цепочку производных

x = dVar value
f = x^3
f' = df f
f'' = df f'

и т.д.

Причём реализация этого очень проста:

data Dif a = C a | D a (Dif a) -- второй конструктор содержит выражение (функцию) и производную.

dVar value = D value 1.0 -- маленький кирпичик, из которого всё собирается.

p@(D x x’) * q@(D y y’) = D (x*y)(x’*q+p*y’) -- правило Лейбница

df (D _ p) = p -- "вычисление" производной (форсируем ленивый thunk)


Например, let x = dVar 3.0 in df (df (x^3)) вернёт нам 18.0

Information

Rating
Does not participate
Location
Россия
Date of birth
Registered
Activity