Да в том-то и дело, что проблемы подобные коровам и траве в реальных проектах встречаются повсеместно, и разработчики далеко не всегда выбирают нужный класс удачно (это во многом зависит от ответственностей, которые навешивают на классы впоследствии). Кроме того, есть много ошибок, связанных с компромиссом "богатый интерфейс + удобство использования vs минималистичный интерфейс + удобство реализации". SOLID опять же, понятие "single responsibility" может варьироваться в широких пределах в зависимости от конкретного человека и его понимания — что для одного single, для другого — multiple.
Теперь как решать указанную задачу в Хаскеле например:
eat :: Cow -> Grass -> Cow
eat cow grass = ... newCow -- да, вернём новую корову
попытка подсунуть траву тигру, или траве корову закончится ошибкой от компилятора. Если же так получилось, что мы вынуждены использовать функцию Grass -> Cow -> Cow, а нам удобно Cow -> Grass -> Cow, то это делается одной строчкой:
-- используем HOF flip :: (a -> b -> c) -> b -> a -> c
eatFlipped = flip eat
Если вам будет угодно, можете flip считать паттерном "Адаптер" на кончиках пальцев (а кроме этого есть ещё более легковесный карринг), её можно определить локально, как только она где-нибудь понадобится.
В любом случае, мне не пришлось решать дилеммы выше (куда приклеить функцию eat, достаточно ли Single Responsible полученная Корова или Трава, и нужно ли добавлять метод size в корову или и так сойдёт (ведь size можно посчитать просто как сумму частей!).
Описать с помощью ООП как корова щиплет траву (там будет корова.щипать(трава), трава.бытьОщипанной(корова) или Природа.поедание(корова, трава)?).
Есть лужа (совсем необязательно являющаяся сечением шара), в лужу бросили камень (вектор скорости совсем необязательно перпендикулярным поверхности). Задача: описать поведение волн с течением времени. Вот здесь бесполезность ООП лично для меня очевидна — задача поставлена, есть необходимость её решать, но ООП здесь совершенно никак не вклеивается — можно конечно создать классы "Лужа" и "Камень", и описать метод "взаимодействовать", но это даже на миллиметр не приблизит нас к моделированию действительности.
PS: А приблизит нас тоненькая брошюрка "Введение в динамику жидкости" товарища Бэтчелора и такая же тоненькая "Вычислительная математика" Тихонова+Самарского. В первой книжке мы найдём как выписать систему уравнений Навье-Стокса для нашей лужи, а во второй книжке мы найдём методы, как решать системы диффур численно. Покроем лужу достаточно мелкой сеткой, напишем алгоритм числнного решения (с прицелом на кластер) и вот тогда мы приблизимся таки к моделированию нашей действительности. И тут — о чудо — появятся объекты, да. Например, vector, matrix, обёртки над сокетами и тому подобные технические сущности. Причём набор этих сущностей будет существенно зависеть от выбранного способа решения. Однако куда делись лужа с камнем?
В Эрланге есть функция с побочным эффектом (send, !, посылка сообщения процессу), и этот эффект настолько мощный, что на базе него реализуетмя и мутабельное состояние, и ввод-вывод, и исключения и много чего ещё. И из-за этого также образуются всё те же проблемы, как в оопе — гонки, дедлоки, лайвлоки и прочие. Благодаря иммутабельности объектов, проблем удаётся избежать на уровне процессов, но проблемы уезжают на уровень взаимодействия между процессами.
То есть вас интересует просто этимология термина monad? Лично я не в курсе, но я уверен, это можно разыскать, было бы желание.
Я бы предпочёл вести более содержательную беседу, чем спор о терминах.
Монада для разработчика на Haskell, например, имеет вполне конкретный смысл безо всяких аналогий (это тайпкласс вместе с требованиями на его инстансы).
Если вам легче, можете это назвать Chainable, Sepulator или LittleFuzzyThing, но тем самым вы уничтожите шанс быть понятым другими.
А аналогии вообще вредны почти всегда, ибо дают ложное ощущение понимания.
Ещё отличный пример: библиотека optparse-applicative, весьма удобна в использовании (пример из документации):
data Sample = Sample
{ hello :: String
, quiet :: Bool
, enthusiasm :: Int }
sample :: Parser Sample
sample = Sample
<$> strOption
( long "hello"
<> metavar "TARGET"
<> help "Target for the greeting" )
<*> switch
( long "quiet"
<> short 'q'
<> help "Whether to be quiet" )
<*> option auto
( long "enthusiasm"
<> help "How enthusiastically to greet"
<> showDefault
<> value 1
<> metavar "INT" )
Вам нужен аналог newtype из Haskell. Эта конструкция создаёт обёртку вокруг какого-то типа, которая гарантированно будет иметь представление в рантайме точно такое же, как и сам тип. При этом достигается типобезопасность.
λ> import Data.Text
λ> newtype Email = Email Text
λ> let f (x :: Email) = True -- определили простейшую функцию от Email
λ> :t f
f :: Email -> Bool
λ> f (Email "xyz") -- передать обёртку можно
True
λ> f "xyz" -- а передать сырой тип нельзя
<interactive>:43:3: error:
• No instance for (Data.String.IsString Email)
Чтобы совсем достичь безопасности, нужно сделать так, чтобы экземпляры типа Email можно было строить только через какую-то функцию mkEmail :: Text -> Maybe Email которая вернёт Nothing, если переданная строка не является на самом деле строкой с емейлом.
Могут, это приветствуется, и вполне заметно улучшает поддерживаемость кода.
Но представим, что все методы чистые. Это значит, что их работа не зависит от состояния инстанса класса, и тогда возникает вопрос: "Зачем эти все методы содержатся в этом классе?"
И есть только один разумный ответ: класс просто группирует эти методы (которые уже просто функции), но если язык позволяет организовывать модули просто с внешними функциями (например, Kotlin, Scala, Rust), то вполне естественно организовать их так.
Продолжая в том же духе, мы можем все классы "раскрыть" таким образом, и получим программу состоящую из функций, организованных в модули и работающих с неизменяемыми данными.
Почти как в Хаскеле.
int doSomething() {
this.data = 42;
return this.data;
}
То что this является неявным аргументом для методов только ухудшает возможности для композиции: состояние класса является мутабельным окружением для всех методов, и ошибок с доступом к такому мутабельному состоянию не счесть. Даже без многопоточности.
+1
Идеальное — это 1. Что-то вошло — что-то вышло. Прямолинейный поток выполнения, композиция, лёгкость переиспользования. Когда у функции нет аргументов, это значит, что она берёт входную информацию из окружения, а значит имеет побочный эффект. Такую архитектуру нельзя называть «чистой».
Вы по всей видимости говорите о Ross Tate который ведёт свою научную деятельность с 2010-го года. Его работа наверняка повлияла на Цейлон, Скалу и Котлин,
Однако Intersection и Union Types были изобретены (открыты?) и изучались задолго (эта статья 1995-й года и есть ещё более ранние) до работ Росса, и появлялись во многих экспериментальных языках. Изучать эту тему мог кто угодно, в том числе и Росс, и Мартин, и другие учёные.
Сама же Scala 3 базируется на компиляторе Dotty, основой которого является исчисление Dependent Object Types. Обоснование этой системы типов выполнено в-основном аспирантами Мартина (Nada Amin, Samuel Grütter, Tiark Rompf и Sandro Stucki). Intersection и Union типы являются частью DOT и используются для описания наименьшей верхней границы (least upper bound) для if- и match- выражений.
Так что 14 лет для построения компилятора, поддержки и развития ветки Scala 2, переосмысления ошибок, создания новой системы типов и доказательства её корректности, построения нового компилятора Dotty и использование его как бэкенда для языка Scala — вот для всей этой работы 14 лет для меня не выглядит сильно уж большим временем.
А должен?
Не сразу понял посыл вашего сообщения, да, не противоречит. Считайте, что я дополнил ваш ответ.
Да в том-то и дело, что проблемы подобные коровам и траве в реальных проектах встречаются повсеместно, и разработчики далеко не всегда выбирают нужный класс удачно (это во многом зависит от ответственностей, которые навешивают на классы впоследствии). Кроме того, есть много ошибок, связанных с компромиссом "богатый интерфейс + удобство использования vs минималистичный интерфейс + удобство реализации". SOLID опять же, понятие "single responsibility" может варьироваться в широких пределах в зависимости от конкретного человека и его понимания — что для одного single, для другого — multiple.
Теперь как решать указанную задачу в Хаскеле например:
попытка подсунуть траву тигру, или траве корову закончится ошибкой от компилятора. Если же так получилось, что мы вынуждены использовать функцию
Grass -> Cow -> Cow
, а нам удобноCow -> Grass -> Cow
, то это делается одной строчкой:Если вам будет угодно, можете
flip
считать паттерном "Адаптер" на кончиках пальцев (а кроме этого есть ещё более легковесный карринг), её можно определить локально, как только она где-нибудь понадобится.В любом случае, мне не пришлось решать дилеммы выше (куда приклеить функцию
eat
, достаточно ли Single Responsible полученная Корова или Трава, и нужно ли добавлять методsize
в корову или и так сойдёт (ведьsize
можно посчитать просто как сумму частей!).Описать с помощью ООП как корова щиплет траву (там будет
корова.щипать(трава)
,трава.бытьОщипанной(корова)
илиПрирода.поедание(корова, трава)
?).Есть лужа (совсем необязательно являющаяся сечением шара), в лужу бросили камень (вектор скорости совсем необязательно перпендикулярным поверхности). Задача: описать поведение волн с течением времени. Вот здесь бесполезность ООП лично для меня очевидна — задача поставлена, есть необходимость её решать, но ООП здесь совершенно никак не вклеивается — можно конечно создать классы "Лужа" и "Камень", и описать метод "взаимодействовать", но это даже на миллиметр не приблизит нас к моделированию действительности.
PS: А приблизит нас тоненькая брошюрка "Введение в динамику жидкости" товарища Бэтчелора и такая же тоненькая "Вычислительная математика" Тихонова+Самарского. В первой книжке мы найдём как выписать систему уравнений Навье-Стокса для нашей лужи, а во второй книжке мы найдём методы, как решать системы диффур численно. Покроем лужу достаточно мелкой сеткой, напишем алгоритм числнного решения (с прицелом на кластер) и вот тогда мы приблизимся таки к моделированию нашей действительности. И тут — о чудо — появятся объекты, да. Например, vector, matrix, обёртки над сокетами и тому подобные технические сущности. Причём набор этих сущностей будет существенно зависеть от выбранного способа решения. Однако куда делись лужа с камнем?
В Эрланге есть функция с побочным эффектом (send, !, посылка сообщения процессу), и этот эффект настолько мощный, что на базе него реализуетмя и мутабельное состояние, и ввод-вывод, и исключения и много чего ещё. И из-за этого также образуются всё те же проблемы, как в оопе — гонки, дедлоки, лайвлоки и прочие. Благодаря иммутабельности объектов, проблем удаётся избежать на уровне процессов, но проблемы уезжают на уровень взаимодействия между процессами.
Я бы предпочёл вести более содержательную беседу, чем спор о терминах.
Монада для разработчика на Haskell, например, имеет вполне конкретный смысл безо всяких аналогий (это тайпкласс вместе с требованиями на его инстансы).
Если вам легче, можете это назвать
Chainable
,Sepulator
илиLittleFuzzyThing
, но тем самым вы уничтожите шанс быть понятым другими.А аналогии вообще вредны почти всегда, ибо дают ложное ощущение понимания.
Сохранение порядка эффектов независимо от способа вычисления (энергичного, параллельного, или какого-нибудь из ленивых — неважно)
Вам нужен аналог
newtype
из Haskell. Эта конструкция создаёт обёртку вокруг какого-то типа, которая гарантированно будет иметь представление в рантайме точно такое же, как и сам тип. При этом достигается типобезопасность.Чтобы совсем достичь безопасности, нужно сделать так, чтобы экземпляры типа
Email
можно было строить только через какую-то функциюmkEmail :: Text -> Maybe Email
которая вернётNothing
, если переданная строка не является на самом деле строкой с емейлом.Мне тоже очень интересно, в Haskell есть всё упомянутое и даже больше, но почему-то вопрос повис в воздухе.
Могут, это приветствуется, и вполне заметно улучшает поддерживаемость кода.
Но представим, что все методы чистые. Это значит, что их работа не зависит от состояния инстанса класса, и тогда возникает вопрос: "Зачем эти все методы содержатся в этом классе?"
И есть только один разумный ответ: класс просто группирует эти методы (которые уже просто функции), но если язык позволяет организовывать модули просто с внешними функциями (например, Kotlin, Scala, Rust), то вполне естественно организовать их так.
Продолжая в том же духе, мы можем все классы "раскрыть" таким образом, и получим программу состоящую из функций, организованных в модули и работающих с неизменяемыми данными.
Почти как в Хаскеле.
Да, конечно.
То что
this
является неявным аргументом для методов только ухудшает возможности для композиции: состояние класса является мутабельным окружением для всех методов, и ошибок с доступом к такому мутабельному состоянию не счесть. Даже без многопоточности.Идеальное — это 1. Что-то вошло — что-то вышло. Прямолинейный поток выполнения, композиция, лёгкость переиспользования. Когда у функции нет аргументов, это значит, что она берёт входную информацию из окружения, а значит имеет побочный эффект. Такую архитектуру нельзя называть «чистой».
Вы по всей видимости говорите о Ross Tate который ведёт свою научную деятельность с 2010-го года. Его работа наверняка повлияла на Цейлон, Скалу и Котлин,
Однако Intersection и Union Types были изобретены (открыты?) и изучались задолго (эта статья 1995-й года и есть ещё более ранние) до работ Росса, и появлялись во многих экспериментальных языках. Изучать эту тему мог кто угодно, в том числе и Росс, и Мартин, и другие учёные.
Сама же Scala 3 базируется на компиляторе Dotty, основой которого является исчисление Dependent Object Types. Обоснование этой системы типов выполнено в-основном аспирантами Мартина (Nada Amin, Samuel Grütter, Tiark Rompf и Sandro Stucki). Intersection и Union типы являются частью DOT и используются для описания наименьшей верхней границы (least upper bound) для if- и match- выражений.
Так что 14 лет для построения компилятора, поддержки и развития ветки Scala 2, переосмысления ошибок, создания новой системы типов и доказательства её корректности, построения нового компилятора Dotty и использование его как бэкенда для языка Scala — вот для всей этой работы 14 лет для меня не выглядит сильно уж большим временем.
Не понимаю, за что минусы, но может хаскелисты обиделись… :-)