Pull to refresh

Comments 51

Экспоненциальные типы (то бишь лямбды)? Экзистенциальные? Зависимые?
экспоненциальных? ))

Экзистенциальные данные — со скрытыми параметрами.

data Counter a = forall self. NewCounter
    { _this    :: self
    , _inc     :: self -> self
    , _display :: self -> IO ()
    , tag      :: a
    }


Зависимые классы помогают выявить зависимость между передаваемыми параметрами класса и примерно выглядят так:
   class Collects e ce | ce -> e where
      empty  :: ce
      insert :: e -> ce -> ce
      member :: e -> ce -> Bool
Экспоненциальные — это лямбды. A -> B.

Зависимые типы — это другое. Их в хаскеле нет, они есть в Agda и прочих Coq-ах.

В Scala есть такая фигня под названием «path-dependent types» (Шишков, переводи сам), это такой маленький кусочек настоящих зависимых типов.

Я просто перечисляю то, что в статье не упомянуто.
На счёт Скала — спасибо, попробую добавить. Эти «путе-зависимые» объекты добавляют межобъектное (одного класса) взаимодействие, но не межклассовое. Правильно?

Зависимые типы — отлично замечено. Я их не рассматривал, так как Agda и Coq используются для доказательств, а не программирования естественных задач.

Лямбда функции, хоть их техническая реализация может отличатся от обычных функций в языке, тем не менее, являются обычными функциями, с такими же возможностями. Были ещё в раннем Лиспе.
Эти «путе-зависимые» объекты добавляют межобъектное (одного класса) взаимодействие, но не межклассовое. Правильно?


Либо я вас не понял, либо не правильно.

Вот в докладе Living in a Post-Functional World с конференции flatMap(Oslo) есть хорошее объяснение что такое path-dependent types. Примерно с 14 минуты идет введение из других языков, пример на скале начинается с двадцатой минуты.

При этом если просто гулить, то зачастую находятся гораздо менее общие объяснения, которые могут сформировать не правильное представление.
В этом докладе рассказывается, какие они построили модули первого порядка, которых нет в Стандартном МЛ. Зато почему-то промолчали, что они есть в ОКамле.
И заодно рассказывают, почему классы типов — это так плохо на Хаскеле, зато так классно в Скале.

Что касается path-dependent types — в докладе об этом не сказано.
Тем не менее, path-dependent types — это разрешение ввергнутся во внутреннее пространство объекта другому объекту, поскольку он имеет такой же тип:
res: java.lang.Class[_ <: VariableName.InnerClassName] = class OuterClassName$InnerClassName
Модули первого порядка и path-dependent types в скале — это одно и то же, на сколько я это понимаю.

Такой модуль — это самый обычный объект.

И либо я не понял, что вы описали, либо это не path-dependent types.

Если у вас есть 2 перменные:

val a: OuterClassName = ???
val b: OuterClassName = ???

To a.InnerClassName и b.InnerClassName — разные типы.

PS: не заметил там негатива в сторону хаскеля.
PS Чего только стоит "#haskellfail" ))
Так это про модули, а не про тайпклассы. Я неоднократно встречал жалобы апологетов хаскеля на отсутствие модулей.

Хаскель — давно и надежно lingua franca ФП. Всерьез наезжать на него ни кто не будет, но идеала не существует.
Модули первого порядка и path-dependent types в скале — это одно и то же, на сколько я это понимаю.

Если это так, то это уже есть в статье: модули первого порядка (не знаю как в Скале, а в ОКамле они к тому же могут быть рекурсивными) — достойная функциональная замена объектам.
По мощности они соизмеримы с объектами.

To a.InnerClassName и b.InnerClassName — разные типы.

Верно, а вместе с
val c: a.type = a

a.InnerClassName и с.InnerClassName — одинаковы
Что означает «разрешение ввергнутся во внутреннее пространство объекта другому объекту, поскольку он имеет такой же тип»?

То, что в вышеприведённом примере `а` не отличит себя от `с`.
Если это так, то это уже есть в статье: модули первого порядка (не знаю как в Скале, а в ОКамле они к тому же могут быть рекурсивными) — достойная функциональная замена объектам.


Я, видимо, не достаточно разобрался с терминологией. Спасибо, что прояснили.

В скале это не замена, это синонимы. Объект называют модулем если его используют как модуль.

Вложенность, рекурсивность, наследование (включая множественное) и все остальное прилагается.

В данном случае это не просто такой же тип, это еще и знание компилятора о том, что это в точности тот же объект/модуль. Крайне полезно если надо объединить 2 модуля, каждый из которых зависит от третьего с использованием одного и того же «третьего».

Звучит запутанно, выглядит чуть проще:

val parser = new { val xc: c.type = c } with XPathParsers


Пример из моего кода.

Я вас не сразу понял, так как для меня фраза "`а` не отличит себя от `с`" (привычка с SO? жаль на хабре не работает) бессмысленна — `a` и `c` это два имени одного объекта/модуля.
Что означает «разрешение ввергнутся во внутреннее пространство объекта другому объекту, поскольку он имеет такой же тип»? Если вы про область видимости private, то это наследство java и есть дополнительно private[this].
Внесу пару замечаний по тексту, дабы восстановить историческую справедливость:
Интерфейсы впервые вводят Ява и Си#. И это понятно – они были лидерами в объектных языках.
Неправда, интерфейсы в чистом виде были еще в Delphi, задолго до Java и C#. Да и полностью абстракный класс в С++ — точно такой же интерфейс.

Дело в том, что пользователи имеют возможность построить любые перечисления. Только вот для работы с ними в Си нет ничего. Вообще ничего. Создавать перечисления можно, а работать с ними — нельзя.
Поскольку Си был в мейнстриме, мало кто хотел добавлять этот тип данных в другие языки.
Очень странный вывод. Опять же надо отметить, что в том же Паскале давным давно был простой инструментарий для работы с перечислениями — функции low(), high(), итерация в цикле for.

На счёт Паскаля — да, я об этом написал.
На счёт Делфи — похоже да, ошибся, исправлю, спасибо
Добавлю ещё замечание по перечислениям — в паскале уже давно есть множества.
set of вместе с перечислениями даёт более мощные возможности, чем побитовые операции. Пересечения, объединений, сумма, вычитание множеств. это гораздо удобнее a |b &c

наглядно:
type
  TEnumType = (etOne, etTwo, etThree, etFour);
  TEnums = set of TEnumType;
var
  operations: TEnums;
begin
  operations := [etTwo, etThree];
  if etOne in operations then Foo;                //если etOne есть во множестве
  if [etTwo, etThree] * operations = [] then Bar; //если etTwo и etThree не входят во множество (то есть если нулевое пересечение, ни одно из значений не встречается)
end;

И это весьма удобно.
Вижу, всё таки есть возможность разночтения. Поэтому внёс правку.

Я даже больше скажу — в Паскале перечисления были с самого начала.

И можно было даже интервалы делать:
operations := [etOne .. etThree];
Простите уважаемый — а почему Generic класс ограниченный интерфейсами по типам-параметрам в Scala или C# — по вашему не предоставляет инструментария для ещё не описанных типов?.. Учитывая неявные приведение, и методы расширения (с параметрами в виде интерфейсов или базовых классов неописанных типов) — очень даже предоставляет.
Generic — метапрограммирование.
Он не предоставляет новые пользовательские данные.
Он «всего лишь» избавляет от рутины написания порождающих Generic классов
Если я правильно понял речь идёт о том что новый класс по определённым признакам должен быть дополнен уже существующими методами, которые были описаны когда никто и не догадывался что такой новый класс понадобиться. В шарпе — метод расширения для интерфейса прекрасно с этой задачей справляется, факт того что класс реализует интерфейс определяется динамически.

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

public static class MyExtensionMethods
{
    public static void MyMethod(this ITestInterface Test)
    {

        Console.WriteLine("Extension Method...");
    }
}
public interface ITestInterface
{
    void Display(string name);
}
public class TestClass : ITestInterface
{
    public void Display(string name)
    {
        Console.WriteLine("Hello" + name);
    }
}
public static void Main()
{
    ITestInterface test = new TestClass();
    test.Display("Mitesh");  //Output - Hello, Mitesh
    test.MyMethod();    // Output - Extension Method...
}
Собственно, что меняет этот пример, относительно простого объекта?

Мы можем добавить новые методы, использующие интерфейс, который вшит в класс.
Да, это более гибко, чем простой интерфейс, однако, ничего более возможностей вшитого интерфейса мы не получим от объекта.
простого объекта какого типа? Можно писать методы расширения и для всех объектов просто будет public static bla name(this object...) — и это не добавит удобства в работе. Добавлять методы, новые функции прям всем объектам подряд — не круто. Почему это мы не получим «ничего более» ???
Будьте добры пример кода где ярко вот эта фича используется и то что по вашему ближайший аналог на еретическом ООП? Так будет нагляднее.
Условно говоря, можно посмотреть на многие объекты как на фреймворки. Их используют, а не правят. Если не походят — просто не используют.
Прежде всего это касается чужих и нетривиальных объектов.

У нас один разработчик написал класс Вектор.
Второй разработчик написал класс Механика (например, для игр).
Мы (третий разработчик) молимся, что бы у нас вышел паттерн Адаптер, что бы Вектор подошёл к Механике.
Ок, вышло. (эта ситуация сама по себе показательна, могло и не выйти)

Но нам нужны не Вектора (слишком банально для заказчика), а Вектор с Хризантемкой.
Тут уже полный тупик — в Механику уже никаких цветочков не вставишь.
И тут приходится выкручиваться: либо отказываться от цветочков, либо изменять Механику, либо добавлять всевозможные костыли.
и всё таки… код в студию пожалуйста…
У нас есть 2 объекта, Механика и Вектор:

1)Более простой вариант — объект Механика, принимающий на вход(сеттер) массив пар чисел, и на выходе(геттер) — новые пересчитанный массив пар чисел.
2)Усложнённый вариант Механики, возвращающий несвязанный массив пар чисел.

Объект Вектора с Хризантемкой:
3)Простой вариант — у каждого Вектора своя хризантема.
4)Усложнённый вариант — при этом хризантема во время движения должна крутиться в противофазно, относительно Вектора, к которому прикреплена.

1+3 можно исправить расширенными методами.
2+3, 1+4 лишь в некоторых случаях можно тоже, но так вряд ли кто будет делать. Будут уже править
несколько запутанная предметная область…
я хочу посмотреть на две версии кода на хаскель и тот как он эволюционировал после появления новых требований.
Хаскель не объектно-ориентированный язык.
В нём нет объектов вообще.

Покажу пример 1+3+4:

Функция
mechanic :: Movement m => m ->m

instance Movement (Double,Double) where
     move (x, y) = (x + dx, y+dy)

instance Movement FlowerVector where
     move (FlowerVector {..}) = FlowerVector {x = x + dx, y = y + dy, 
                             flower_x = flower_x - dx, flower_y = flower_x - dy,
                             flower = flower}

Можете добавить свои данные. Например, как зависит цвет, форма цветка в зависимости от тех или иных событий.
Можно вообще любые данные обрабатывать, только надо написать экземпляр instance Movement MyData
итак для того что бы обрабатывать «любые данные» нужно определить это поведение для этих новых инстанс типов данных, так?
Смысл моего вопроса — в том как эволюционирует код на хаскель.
Вероятно разница в том как этот код можно использовать дальше — я бы хотел увидеть пример использования этого кода и то что по вашему мимикрирует этот подход в более традиционном ООП — а лучше всего с использованием Extenstion methods.
Есть такая замечательная штука как Generic Extension Methods — можно расширить не знаю какие типы, но все которые реализуют определённый интерфейс. Мне это кажется наиболее близкой идеей, если я вас правильно понял.
Да, это наиболее близко.
Разница заключается в доступе изменения данных.
Интерфейсы дают доступ лишь к прописанным(в интерфейсе) полям, а инстансы — ко всем полям.

Пы.Сы. Кстати, в Хаскеле есть свои Генерики. Даже не один, а 3 направления
— Generic
— Template
— Data
wiki/Concepts_(C++)

Вот почти та же самая идея для Cи++, что и классы типов для Хаскеля
Код на Хаскеле несколько отличается от ООП-шного.
И развитие кода тоже имеет несоответствия.
В основном код излишне общ.
И в большинстве случаев достаточно просто использовать код.
При этом нет нужны во всяких фабриках и фабриках фабрик.
Из-за обобщённости нет необходимости изменять поведение, достаточно дописать функционал.
Если и нужно менять, то в основном, добавляется очень малое количество кода — до нескольких строчек.
Хотя конечно же, если код не оптимальный — изменения могут быть существенными.

Необходимо отметить, на Хаскеле данными могут быть не только данными, а и действиями, или совокупностью.
Скрытый текст
Например, монады — это данные, с которыми можно работать в императивном стиле, стрелки — данные, с которыми можно работать как блок-схемами, линзы — данные, с которыми можно работать как с объектами.

Поэтому на Хаскеле не надо отдельно заниматься данными, отдельно действиями, отдельно архитектурой.

Если интересна именно эволюция кода, советую почитать интересную статью автора библиотеки, где он описывает, что он добавил в неё.
pipes-3.3.0:…
отрывок
pipes-2.4 впервые идентифицировал существование трёх дополнительных категорий, два из которых я назвал «request» и «respond». Эти категории очень полезны, особенное, если можно использовать ListT с обоими. Но я открыл, что нельзя использовать (/>/) и (\>\) композиции операторов для некоторых прокси-траснформеров, особенно
MaybeP
EitherP
StateP
WriterP
Это очень разочаровывало и казалось действительно странным, что прокси-трансформер может поднять одинаковость этих категорий (т.е. request и respond), но не всегда может поднять соответственную им композицию операторов.
Я принял временное решение, отделить (/>/) и (\>\) в отдельный ListT класс типов.

(работая с pipes-directory и ExceptionP обнаружилось), ExceptionP — это всего лишь синоним EitherP, а EitherP не имплементировал ListT класс, а это означало, что я не мог использовать монаду ProduceT. Поэтому я пересмотрел EitherP и открыл, что есть закон, по которому можно сделать инстанс EitherP для ListT, тот, который я искал ранее. Более того, я мог использовать то же самое для применения MaybeP к ListT.
А это означало, что только 2 прокси-трансформера остались без импементирования ListT:
StateP
WriterP
Кроме того, WriterP внутренне определялся под полом через StateP, означая, что если я смогу найти решение по StateP, я смогу объединить ListT назад к Proxy классу.
Попутно, работая с pipes-parse, я открыл несколько неверных частных случаев для StateP, которые давали неверное поведение. Также оказалось, что и WriterP тоже давал неверное поведение в широких вариантах случаев.
Это означало, что я имплементировал оба этих прокси-трансформера неверно, поскольку оба давали много неверных результатов для многих случаев. И оба этих трансформера не могли быть подключены к ListT.
Это наблюдение позволило мне открыть правильное решение: разрешить StateP и WriterP делится эффектами глобально в линии (pipeline), вместо локально.
Это решение фиксировало обе проблемы:
— оба могли имплементировать List и подчинялись законам ListT
— Оба давали правильное поведение во всех частных случаях.
Вследствие этого, я снова объединил ListT к классу Proxy и объединил request и respond к их соответствующим композитным операторам.
Ну, и сейчас все прокси-трансформеры поднимают все четыре категории верно.
Предложения по применению расширенной (девятиуровневой) модели взаимодействия открытых систем

functional — Функциональное — для разделов, использующих функциональное программирование.
aspect — Аспектное — для интерфесов и аспектов. Для методов могут указываться необходимые свойства и дополнения, используемые перед методом (before), после метода (after) и при выполнении каждого оператора (invariant)
predicate — Логическое — соответствует Булевым переменным, доказательству теорем, Аристотелевкой логике, работе с запросами SQL, LINQ или простейшим операциям Prolog.
controller — Управляющее — соответствует контроллеру (Controller) модели MVC
publish — Изменяемое — соответствует представлению (View) для модели MVC
public — Соединяющее — соответствует модели базы данных (Model) модели MVC
protected — Защищенное — внутренние элементы класса
private — Внутреннее — скрытые элементы класса
local — Блоковые — переменные методов и блоков

При этом должны выделяться классы, описывающие функционал без физической реализации, реализующие структуры и использование для конкретных применений

подробнее habrahabr.ru/post/176249/
А где применение-то?
У меня — так в процессе вызревания идеи :-)
Но эти фрагменты «вылазят» в существующих языках и технологиях.

На мой взгляд, для полного задействования необходимо иметь описания предметных областей на уровнях абстрактных классов, распределенных по своим (предметной области) пространствам имен.
Я считаю, что после проверки орфографии это вполне может пойти в золотой фонд Хабра.
Расскажите, пожалуйста, для тех, кто не в курсе, чем хаскелевские классы типов отличаются от type class в scala (оно же context bounds).
Хороший вопрос, как отличаются.
Одно можно сказать точно, это попытка ввести хаскелевские классы типов в Скала.
В скале нет классвов типов, они эмулируются с помощью неявных параметров. Context bounds — синтаксический сахар для этого, причём не всегда применимый.

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

Если кратко в хаскеле можно описывать сложные классы, но нельзя манипулировать несколькими инстансами. В скале _очень_ сложно описывать сложные классы, но инстансы являются first-class values, со всеми вытекающими.
Спасибо. Про скалу я в курсе, было интересно про Хаскель.

То есть я правильно понимаю, что в хаскеле для одного типа не получится в части программы использовать одну реализацию тайпкласса, а в части — другую?

Кстати, что подразумевается под _очень_ сложно ? Тайпкласс — трейт. Реализация — создание экземпляра.
В разных модулях можно, в рамках одного — один статический импорт. Ну и в принципе instance не first class value, хотя подозреваю что есть расширения на эту тему.

Моё любимое: github.com/scalaz/scalaz/blob/v7.0.0/core/src/main/scala/scalaz/std/Either.scala#L74 Я это пробовал читать частями, по 10ку лексем за вечер, на 3й забил (:
Нечитаемость — общее свойство scalaz. Даже shapeless кажется на таком фоне простым и понятным.

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

Единственно что громоздко — определение типа `l``. Остальное сложно не из-за синтаксиса, а из-за сложности реализуемых абстракций на мой взгляд.

Scalaz — это постоянный эксперимент. Не думаю, что большую часть оттуда у кого-то возникнет желание использовать в реальных проектах.
К методам нет претензий, проблема в сигнатуре самого объекта-инстанса. И в том что говорит компилятор когда инстанс не находится. Ну и отдельные функции доставляют: def liftM[G[_[_], _]](implicit G: MonadTrans[G]): G[F, A] = G.liftM(self)
То есть я правильно понимаю, что в хаскеле для одного типа не получится в части программы использовать одну реализацию тайпкласса, а в части — другую?

Можно, если эти реализации закрыты для экспорта/импорта друг к другу.
  module TestZero (Test(..)) where  -- TestZero.hs
     data Test = Test1 | Test2

  module TestOne(testf1)  where     -- TestOne.hs
    import TestZero
    instance Eq Test where
       _ == _ = True
    testf1:: Test -> Test -> Bool
    testf1 = (==)

  module TestTwo(testf2)  where     -- TestTwo.hs
    import TestZero
    instance Eq Test where
      Test1 == Test2 = True
      Test2 == Test1 = True
      _     == _     = False
   testf2:: Test -> Test -> Bool
   testf2 = (==)

  module TestThree  where          -- TestThree.hs
    import TestZero
    import TestOne
    import TestTwo
    test  =  (testf1 Test1 Test2) == (testf2 Test1 Test2)
> АТД вместе с классами типов выглядят грубо, но способны на многое
Да я вас умоляю!.. Божественно они выглядят. Система типов в Хаскеле изящна с математической точки зрения и удобна с практической.
тема безусловно интересная и похоже, что автор обладает поистине энциклопедическими знаниями в этой области. однако читать такой поток сознания с картинками совсем не просто. :)
Указатели впервые появились не в С, а (из распространенных языков) в PL/1. В языке Алгол-68 их рафинировали, а уже оттуда они попали в С.
Указатели впервые появились не в С, а (из распространенных языков) в PL/1.

Гм, просмотрел я «PL/I: Language Specifications. 1965» — не нашёл там такого.
Появились впервые в ассемблере.
В Алголе-68 есть что-то, а Паскаль куда более близкий к современному понимаю указателей, он не до конца их добавил.
И уже Си их добавил в самодостаточном варианте.
Тип хранилища CONTROLLED и BASED, операторы ALLOCATE и FREE, тип переменной POINTER и OFFSET. Практически ничем от С не отличаются. Возможно, это были расширения от IBM, но весьма распространенные.

В Алголе 68 было все, примерно как в Паскале, но при очень вычурном синтаксисе. Отдельный модификатор типа ref (имя), разыменование и генератор переменной в стеке/куче.
Хороший исторический срез, это полезно знать. В некоторых местах не помешало бы добавить примеров и побольше подробностей.
Спасибо за интересную статью!
Sign up to leave a comment.

Articles