Pull to refresh
33
0
Nick Linker @nlinker

Пользователь

Send message

Checked exceptions такие как в джаве не изоморфны алгебраическим типам. Почему?
Попробуйте ответить на вопрос: какая сигнатура будет у функций map и filter в случае checked exceptions?

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


В вашем примере для (гипотетической) f: Fn(A) throws E -> B по-видимому catch вернёт нечто вроде R<B> и вы получите вектор из этих зиначений: Vec<R<B>>.


А чтобы дальше работать с этими R, нужно будет научиться составлять композицию значений R<A> и Fn(A) -> R<B> для любых R, A и B (хотя R лучше бы удовлетворять трём законам).

Исключения являются эффектами, а возвращаемые значения — нет. Разница не только синтаксическая:


xs.iter().map(|x| f(x)).collect::<Vec<_>>()

Если f бросает исключение, то это серьезно мешает композиции функций, особенно в ленивых и асинхронных вычислениях.

А на Собрании Паутины делают ржавый подъёмный кран.


Лопата

Cranelift для WebAssembly

Откуда столько странных мифов про Rust? Вполне есть сабтайп полиморфизм, точно так же базовый интерфейс (типаж Animal), точно такая же диспетчеризация в рантайме, если нужно.


fn main() {
    let cat1 = Cat { color: Color::White };
    let cat2 = Cat { color: Color::Black };
    let cat3 = Cat { color: Color::Grey };
    let dog = Dog { kind: 2 };
    let animals: Vec<&Animal> = vec![&cat1, &cat2, &cat3, &dog];
    for animal in animals {
        animal.sound();
    }
}

(https://pastebin.com/6gKVFvF5)


Другое дело, что это можно часто заменить другими средствами, так что даже в больших и сложных системах необходимости в наследовании не возникает.

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


Пример:


import qualified Data.Map as Map

main = do
    let hm = buildMap
    putStrLn $ "hm = " <> show hm

buildMap =
    let x1 = Map.empty
        x2 = Map.insert "a" 1 x1
        x3 = Map.insert "b" 2 x2
    -- x1 and x2 will be garbage collected
    in x3

Вот, можете посмотреть эту презентацию, начиная с functional list-а, там, я надеюсь, всё достаточно очевидно.

Добавлю, что если определить операцию >=> (называется "композиция Клейсли" (Kleisli composition)) таким образом:


(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
(f >=> g) = \x -> f x >>= g

Можно тогда определить операции bind (который >>=) и join через эту операцию >=>, так что определение монады можно эквивалентно делать через разные операции, они всё равно друг через друга выражаются.


Тогда монадические законы становятся намного проще для запоминания и понимания:


  1. f >=> return ≡ f
  2. return >=> g ≡ g
  3. (f >=> g) >=> h ≡ f >=> (g >=> h)

То есть return оказывается правой и левой единицей в такой структуре, и операция >=> обладает ассоциативностью.
Такие структуры называют страшным словом моноид, но на самом деле ничего страшного нет, такими свойствами обладают множество вещей: числа относительно сложения, числа относительно умножения, строки относительно конкатенации, множества относительно объединения и так далее.

А автор https://github.com/skypjack/entt утверждает, что у него самы быстрый и крутой :-)


Nowadays, EnTT is finally what I was looking for: still faster than its competitors, lower memory usage in the average case, a really good API and an amazing set of features. And even more, of course.

Но зато можно причесать и облагородить API, делая такую "над-обёртку".

Да, мне тоже было трудно обнаружить. Почему-то ссылка на блог-пост о книге, а не на саму книгу "Архитектура приложений с открытым исходным кодом"
http://aosabook.org/en/index.html

Для масштабов "отставания" можно ещё привести в пример суперскалярную архитектуру, которая была разработана у нас в 1978 году, на 16 лет раньше, чем в пентиумах (1994 год).
Также можно привести, как мы отставали в создании защищённых архитектур памяти в Эльбрус, и это тоже не имело аналогов нигде.
Вообще, всем интересующимся советовал бы прочитать Ершовские Лекции, pdf, то, чем занимались учёные и инженеры в то время.

Ну хорошо, я рад, что мы понимаем друг друга :-)

Я согласен, что ООП и ФП не противоречат друг другу.


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


Правильнее будет наоборот, с помощью мутабельных функций построить начальное состояние иммутабельной структуры, а потом спокойно использовать её, пользуясь преимуществами ссылочной прозрачности чистых функций, без проблем разделять структуру между скоупами и потоками и тому подобное. (Этот подход даже в Java используется, см StringBuilder и String).

Вот, кстати, да.


Если IDE легко справляется с выводом типов в программе на динамически типизированном языке (правильно подсказывает допустимые операции, находит точки использования и не ошибается при переименовании), это просто означает, что в программе никакого динамизма и нет вовсе.


Можно было бы тогда взять просто статически типизированный язык, и тем самым воспользоваться уже имеющейся проверкой типов и преимуществами с Type-Driven Development.

В таком виде методы становятся просто чистыми функциями, а состояние классов передаётся и возвращается через композицию этих функций.


Вопрос: зачем тогда нужны классы, кроме как формировать пространство имён для этих функций?
Без мутабельного состояния вообще термины "класс" и "метод" теряют смысл, нет?

Дело в том, что в примере выше конструктор вызывается, а деструктор — нет. То есть создан такой сценарий использования lock_guard_ext, при котором идиома RAII развалилась, и это кмк весьма неприятно.

Это ведь термины, "статическая" и "динамическая" типизация, они ровно это и означают — есть отдельная стадия typecheck в компиляторе или нет.
Можно дальше рассуждать, насколько глубокая проверка типов, должны ли быть типы известны до этой проверки, насколько широкий класс проверок покрывают типы...


Так что вынужден не согласиться: данная стадия компиляции или существует, или нет. И типизация тоже существует. :-)

Я считаю это кумулятивным эффектом от следующих фич C++: исключения, в том числе и в конструкторе, неопределённого порядка передачи параметров при конструировании {}, правил вызова конструкторов/деструкторов во время исключений и плюс эффект какого-то очень нетривиального бага в gcc.
Во-всяком случае если этот код чуть-чуть подправлять по-разному, то баг исчезает. И вдобавок не воспроизводится в clang (но вероятно, там свои тоже очень нетривиальные).

(Из-за какого-то бага комментарий выше оказался не дописан)


Да, вполне номинативны.


class A:
  pass

class B:
  pass

print(print(type(A) == type(B))) # False

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


Номинативность предполагает тождественность по имени, но в контексте интерфейсов, при динамической типизации нас интересует то, подходит ли объект по структуре, а не по имени.

Да, именно так. В Python последствия того, что классы номинативны практически не заметны, поскольку, как вы написали, в подавляющем большинстве случаев от объектов нам нужны методы и поля, а не имена классов.


В целом способность библиотек к кооперации между собой в динамических языках выше именно из-за того, что нет привязки к заранее сконструированным интерфейсам. Интерфейсы задаются неявно в момент использования в пользовательском коде, а не в момент конструирования класса автором библиотеки.

Но подождите, как пользователь сможет пользоваться неизвестным значением? Интерфейс всё же также задаётся автором библиотеки, но неявно, в виде какой-то функциональности, которую автор реализует.


Например, пользователь написал x.foo(). Значит ли это, что в библиотечном интерфейсе появился метод foo? Нет. Этот метод там может быть (возможно в виде недокументированного кода или бэкдора), и уже является частью интерфейся, либо его там может не быть вообще. В обоих случаях интерфейс сформирован автором библиотеки.


Единственная разница лишь в том, что в следующей версии библиотеки автор добавляя метод foo неявно магическим образом меняет определение в случае динамических языков, а в случае статических он выставляет новый номинативный тип (и это вызывает зубную боль, да, MyInterface2), или мог бы выставить обновлённую версию структурного типа.

Information

Rating
Does not participate
Registered
Activity