Как исправить мои тенденции ООП при программировании в Haskell
У меня есть эта повторяющаяся проблема при программировании в Haskell. В какой-то момент я пытаюсь моделировать подход ООП. Здесь я писал какой-то ИИ для флеш-игры, которую я нашел, и я хотел бы описать различные части и уровень как список частей.
module Main where
type Dimension = (Int, Int)
type Position = (Int, Int)
data Orientation = OrienLeft | OrienRight
data Pipe = Vertical | Horizontal | UpLeft | UpRight | DownLeft | DownRight
data Tank = Tank Dimension Orientation
data Bowl = Bowl Dimension
data Cross = Cross
data Source = Source Dimension
-- desired
-- data Piece = Pipe | Tank | Bowl | Cross | Source
-- So that I can put them in a list, and define
-- data Level = [Piece]
Я знаю, что должен абстрагироваться от функциональности и помещать их в список, но я часто чувствую себя заблокированным в процессе написания кода. Каково общее мышление, которое я должен иметь в этих ситуациях?
Ответы
Ответ 1
Вы находитесь на пути к отличному коду. Позвольте мне сделать еще несколько шагов к решению, подобному Haskell.
Вы успешно смоделировали каждый Piece
как независимый объект. Это выглядит прекрасно, как есть, но вы хотите иметь возможность работать с коллекциями штук. Самый непосредственный способ сделать это - описать тип, который может быть любой из желаемых частей.
data Piece = PipePiece Pipe
| TankPiece Tank
| BowlPiece Bowl
| CrossPiece Cross
| SourcePiece Source
который позволит вам написать список таких элементов, как
type Kit = [Piece]
но требует, чтобы, когда вы потребляете Kit
, который соответствует шаблону, для разных типов Piece
s
instance Show Piece where
show (PipePiece Pipe) = "Pipe"
show (TankPiece Tank) = "Tank"
show (BowlPiece Bowl) = "Bowl"
show (CrossPiece Cross) = "Cross"
show (SourcePiece Source) = "Source"
showKit :: Kit -> String
showKit = concat . map show
Также существует веский аргумент для уменьшения сложности типа Piece
путем "сглаживания" избыточной информации
type Dimension = (Int, Int)
type Position = (Int, Int)
data Orientation = OrienLeft | OrienRight
data Direction = Vertical | Horizontal | UpLeft | UpRight | DownLeft | DownRight
data Piece = Pipe Direction
| Tank Dimension Orientation
| Bowl Dimension
| Cross
| Source Dimension
который устраняет многие избыточные конструкторы типов за счет того, что больше не может отражать, какую часть у вас есть в типе функции - мы больше не можем писать
rotateBowl :: Bowl -> Bowl
rotateBowl (Bowl orientation) = Bowl (rotate orientation)
но вместо этого
rotateBowl :: Piece -> Piece
rotateBowl (Bowl orientation) = Bowl (rotate orientation)
rotateBowl somethingElse = somethingElse
что довольно раздражает.
Мы надеемся, что некоторые из компромиссов между этими двумя моделями. Там есть хотя бы одно "более экзотическое" решение, которое использует классы типов и ExistentialQuantification
, чтобы "забыть" обо всем, кроме интерфейса. Это стоит изучить, поскольку это довольно соблазнительно, но считается анти-шаблоном Haskell. Сначала я опишу его, а затем поговорим о лучшем решении.
Чтобы использовать ExistentialQuantification
, мы удалим тип суммы Piece
и создаем класс типа для кусков.
{-# LANGUAGE ExistentialQuantification #-}
class Piece p where
melt :: p -> ScrapMetal
instance Piece Pipe
instance Piece Bowl
instance ...
data SomePiece = forall p . Piece p => SomePiece p
instance Piece SomePiece where
melt (SomePiece p) = melt p
forgetPiece :: Piece p => p -> SomePiece
forgetPiece = SomePiece
type Kit = [SomePiece]
meltKit :: Kit -> SomePiece
meltKit = combineScraps . map melt
Это антипаттерн, потому что ExistentialQuantification
приводит к более сложным ошибкам типа и стиранию множества интересной информации. Обычный аргумент гласит, что если вы собираетесь стереть всю информацию, кроме возможности melt
Piece
, вы должны были просто расплавить ее для начала.
myScrapMetal :: [ScrapMetal]
myScrapMetal = [melt Cross, melt Source Vertical]
И если у вашего класса есть несколько функций, возможно, ваша реальная функциональность сохраняется в этом классе. Например, допустим, что мы можем melt
a Piece
, а также sell
, возможно, лучшая абстракция будет следующей
data Piece = { melt :: ScrapMetal
, sell :: Int
}
pipe :: Direction -> Piece
pipe _ = Piece someScrap 2.50
myKit :: [Piece]
myKit = [pipe UpLeft, pipe UpRight]
Честно говоря, это почти то, что вы получаете с помощью метода ExistentialQuantification
, но гораздо более непосредственно. Когда вы удаляете информацию о типе с помощью forgetPiece
, вы оставляете только словарный словарь для class Piece
--- это точно продукт функций в классе типов, который является тем, что мы явно моделируем с помощью только что описанного типа data Piece
.
Единственная причина, по которой я могу придумать использовать ExistentialQuantification
, лучше всего иллюстрирует система Haskell Exception
- если вам интересно, посмотрите, как она реализована. Короче говоря, он должен был быть сконструирован таким образом, чтобы любой мог добавить новый Exception
в любой код и его можно было бы маршрутизировать через общий механизм Control.Exception
, сохраняя при этом достаточную личность, чтобы пользователь мог ее поймать, Для этого потребовалось также оборудование Typeable
... но оно почти наверняка переполнено.
Вывод должен заключаться в том, что используемая вами модель будет зависеть от того, как вы в конечном итоге потребляете свой тип данных. Исходные кодировки, в которых вы представляете все как абстрактный ADT, как решение data Piece
, хороши тем, что они выбрасывают небольшую информацию... но также могут быть как громоздкими, так и медленными. Заключительные кодировки, такие как словарь melt
/sell
, часто более эффективны, но требуют более глубокого знания о том, что означает Piece
"и как он будет использоваться.
Ответ 2
По моему мнению, нет никакой проблемы в том, как вы думаете: она довольно абстрактная, и это хорошо.
Как предложенный Sassa NF, вы можете использовать классы типов, и это было бы очень элегантно. Но в вашем примере я бы расширил его "более простым способом", используя абстрактный тип данных, поскольку кажется, что это "естественный путь", который ваше мышление.
В этом смысле ваш пример будет похож на, например:
data Piece = Vertical
| Horizontal
| UpLeft
| UpRight
| DownLeft
| DownRight
| Cross
| Bowl Dimension
| Source Dimension
| Tank Dimension Orientation
И повторяя это: я не вижу проблем в вашем способе моделирования проблемы, поскольку для меня это кажется достаточно абстрактным.
Ответ 3
У меня были подобные проблемы 10 лет назад, когда я узнал, что haskell.
Позвольте мне ответить на это более общим способом, чем ваш пример выше.
TL; ДР:
Подумайте в терминах объектов, подобных ООП, которые представляют объекты-значения и объекты-функции, с интенсивным использованием шаблонов С++/javaGenerics. Дизайн вашей программы должен основываться на потоке данных через функции вместо ссылки на переменную память, в которой хранятся состояния промежуточных объектов. Эти функциональные объекты могут быть скомпилированы во время выполнения.
Ваше выражение "В какой-то момент я пытаюсь моделировать подход ООП". описывает ваш опытный способ разработки программ.
Как вы, возможно, слышали где-то, функциональное программирование считается более сложным для программистов ООП, чем для новичков, потому что "неправильные концепции" нужно сначала отучить. Это не совсем так, если вы знаете, как использовать это в своих интересах:
Трюк состоит в том, чтобы использовать ваш опыт ООП или, более вероятно, ваше воображение на "объектах" в обратном направлении: как бы вы создавали функции haskell и значения haskell с объектами? Как бы вы реструктурировали свою программу для разработки разумного потока данных, используя только эти новые концепции? В функциональном программировании каждое "значение" - это объект с только конечными/константными свойствами - или с геттером, который допускает ленивую инициализацию (и тем самым позволяет реализовать, казалось бы, бесконечные одиночные связанные списки). "Функции" - тоже такие "ценности". Если вы думаете о своей программе, используя эти концепции, вы легко освоите ее без шаблонов проектирования ООП, но с "объектами", которые на самом деле являются функциями и значениями.
Если ООП управляет ячейками памяти, в которых хранятся изменяемые состояния/значения, то функции вычисляют следующее состояние/значение из предыдущего. В какой-то момент вы увидите, что вы только думаете о объединении функций для правильного потока данных, а не об управлении ячейками памяти - это-хранилища-значения.
Следующим шагом будет просмотр экземпляров классов типов как каких-то глобально индуцированных словарных объектов, которые автоматически передаются как невидимые параметры для функций. Классы ООП этих словарных объектов будут использовать С++ Templates/javaGenerics в качестве параметров типа, а их методы относятся к их параметрам и/или возвращаемому значению вместо любой ссылки "this". Как только вы поймете, когда и как использовать классы типа, они станут свойствами/ролями/ароматами ваших типов вместо мнимых объектов.
ИМХО, типы классов - один из лучших/самых удачных шаблонов ООП, но они известны только очень немногим программистам и не легко заново изобретаются без функционального мышления. (Каждый опытный программист haskell, которого я знаю лично, изобрел классы типа в качестве шаблона проектирования в С++ Templates/objC/js...)
Общий способ научиться думать в haskell или любом другом новом странном языке - задавать вопрос: "Что я могу сделать с этим языком легко и как?"
И не вопрос: "Как я могу написать этот конкретный проект, который действительно подходит для совершенно другого языка, на котором я уже очень опытен?" (Это звучит очевидно, но мы склонны забывать об этом. Снова и снова.)
Если ваша программа действительно нуждается в объектах больше в смысле ООП, тогда вы можете искать записи хэскелл (если вы новичок).
Если эти объекты представляют собой базы данных или подобные вещи, вы можете искать библиотеки объективов; но линзы могут быть (или не могут быть) очень продвинутыми/раздражающими для начинающих.
Объективы являются составными "объектами" getterAndSetter (или значениями или функциями, подобными вещам), которые используются как "obj.lens1.lens2.modify()" в ООП, которые находятся в ООП, но не являются ни составными, ни самими объектами.
Ответ 4
Вы думаете о полиморфизме. В Haskell есть место и для этого, только это делается по-другому.
Например, кажется, что вы хотите обрабатывать Pieces в Уровне в общем виде. Что это за обработка? Если вы можете определить эти функции, вы обнаружите, что, как определение интерфейса Piece. В Haskell это будет typeclass (определяемый как class Piece a
со списком функций, который должны выполнять "реализации" ).
Затем вам нужно будет определить, что эти функции выполняют для определенных типов данных, например instance Piece Pipe
, и добавить определения этих функций. После того как вы сделали это для всех типов данных, вы можете добавить их в список Pieces.