Подтвердить что ты не робот

Как исправить мои тенденции ООП при программировании в 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]

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

4b9b3361

Ответ 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.