Да в том-то и дело, что проблемы подобные коровам и траве в реальных проектах встречаются повсеместно, и разработчики далеко не всегда выбирают нужный класс удачно (это во многом зависит от ответственностей, которые навешивают на классы впоследствии). Кроме того, есть много ошибок, связанных с компромиссом "богатый интерфейс + удобство использования 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 лет для меня не выглядит сильно уж большим временем.
> Ну а с 64-битным кодом всё еще проще, его просто нет. Игра писалась 20 лет назад =)
Чуть подкорректирую: оригинальный Старкрафт, он да, 32-битный, но OpenBW может в x64.
Логику ботов вполне можно писать под OpenBW, а потом компилировать в Win32 таргет, запустить под оригинальным Старкрафтом, убрать шероховатости, если есть.
Это неплохой вариант, если кому-то некомфортно под виндой (например, мне :-))
А должен?
Не сразу понял посыл вашего сообщения, да, не противоречит. Считайте, что я дополнил ваш ответ.
Да в том-то и дело, что проблемы подобные коровам и траве в реальных проектах встречаются повсеместно, и разработчики далеко не всегда выбирают нужный класс удачно (это во многом зависит от ответственностей, которые навешивают на классы впоследствии). Кроме того, есть много ошибок, связанных с компромиссом "богатый интерфейс + удобство использования 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 лет для меня не выглядит сильно уж большим временем.
Не понимаю, за что минусы, но может хаскелисты обиделись… :-)
Чуть подкорректирую: оригинальный Старкрафт, он да, 32-битный, но OpenBW может в x64.
Логику ботов вполне можно писать под OpenBW, а потом компилировать в Win32 таргет, запустить под оригинальным Старкрафтом, убрать шероховатости, если есть.
Это неплохой вариант, если кому-то некомфортно под виндой (например, мне :-))