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

Наследование для расширения структуры данных в Haskell

Программист на С++, который пытается изучить Haskell здесь. Пожалуйста, извините, наверное, простой вопрос. Я хочу перевести программу, представляющую трехмерные фигуры. В С++ у меня есть что-то вроде:

class Shape {
public:
  std::string name;
  Vector3d position;
};

class Sphere : public Shape {
public:
  float radius;
};

class Prism : public Shape {
public:
  float width, height, depth;
};

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

В С++ функция-член может просто получить доступ к этим параметрам, но мне сложно определить, как это сделать в Haskell с записями или типами классов или что-то еще.

Спасибо.

4b9b3361

Ответ 1

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

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

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

Для решения типа typeclass я лично не хотел бы отражать иерархию классов формы, но создавать классы типов для "вещей с площадью поверхности", "вещи с томом", "вещи с радиусом",...

Это позволяет вам писать функции, которые принимают конкретные виды фигур, например сферы, только (поскольку каждая форма является ее собственным типом), но вы не можете написать функцию, которая принимает "любую форму", а затем выделяет различные бетоны виды форм.

Ответ 2

Прямой перевод.

type Vector3D = (Double, Double, Double)

class Shape shape where
    name :: shape -> String
    position :: shape -> Vector3D

data Sphere = Sphere {
    sphereName :: String,
    spherePosition :: Vector3D,
    sphereRadius :: Double
}

data Prism = Prism {
    prismName :: String,
    prismPosition :: Vector3D,
    prismDimensions :: Vector3D
}

instance Shape Sphere where
    name = sphereName
    position = spherePosition

instance Shape Prism where
    name = prismName
    position = prismPosition

Обычно вы этого не делали; это повторяющиеся и полиморфные списки требуют языковых расширений.

Вместо этого, прикрепление их к одному закрытому типу данных, вероятно, является первым решением, на которое вы должны пойти.

type Vector3D = (Double, Double, Double)

data Shape
  = Sphere { name :: String, position :: Vector3D, radius :: Double }
  | Prism { name :: String, position :: Vector3D, dimensions :: Vector3D }

Вы можете, конечно, смоделировать несколько уровней наследования, создав больше типов:

class (Shape shape) => Prism shape where
    dimensions :: Vector3D
data RectangularPrism = ...
data TriangularPrism = ...
instance Prism RectangularPrism where ...
instance Prism TriangularPrism where ...

Вы также можете имитировать его, вставляя типы данных.

type Vector3D = (Double, Double, Double)

data Shape = Shape { name :: String, position :: Vector3D }

data Sphere = Sphere { sphereToShape :: Shape, radius :: Double }
newSphere :: Vector3D -> Double -> Shape
newSphere = Sphere . Shape "Sphere"

data Prism = Prism { prismToShape :: Shape, dimensions :: Vector3D }

data RectangularPrism = RectangularPrism { rectangularPrismToPrism :: Prism }
newRectangularPrism :: Vector3D -> Vector3D -> RectangularPrism
newRectangularPrism = (.) RectangularPrism . Prism . Shape "RectangularPrism"

data TriangularPrism = TriangularPrism { triangularPrismToPrism :: Prism }
newTriangularPrism :: Vector3D -> Vector3D -> TriangularPrism
newTriangularPrism = (.) TriangularPrism . Prism . Shape "TriangularPrism"

Но симулирование OO в Haskell не так близко, как удовлетворительно, как на самом деле мышление в стиле Haskellish. Что вы пытаетесь сделать?

(Также обратите внимание, что все эти решения разрешают только выбросы, downcasting является небезопасным и запрещенным.)

Ответ 3

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

data Shape  = Shape  { name        :: String, position :: Vector3d }
data Sphere = Sphere { sphereShape :: Shape,  radius   :: Float }
data Prism  = Prism  { prismShape  :: Shape,  width    :: Float, height :: Float, depth :: Float }

Другими словами, модель ссылается на суперклассы в качестве дополнительных полей в вашем типе данных. Он легко распространяется на более длинные цепочки наследования.

Не используйте классы типов, например, предлагает эфемер. Они используются для перегрузки функций, и это совсем не проблема: ваш вопрос касается моделирования данных, а не поведения.

Надеюсь, это поможет!

Ответ 4

Простой перевод разворачивает ту часть, которая меняется, но позволяет избежать типов:

type Vector3D = (Float,Float,Float)
data Body = Prism Vector3D | Sphere Double
radius (Prism position) = -- code here
radius (Sphere r) = r

затем

data Shape = Shape {
  name :: String,
  position :: Vector3D,
  body :: Body
}

shapeOnly (Shape _ pos _) = -- code here

both = radius . body

sphereOnly (Shape _ _ (Sphere radius)) = -- code here
sphereOnly _ = error "Not a sphere"

Это не очень простой вопрос. Конструкция структуры данных очень отличается между С++ и Haskell, поэтому я уверен, что большинство людей, поступающих с языка OO, спрашивают одно и то же. К сожалению, лучший способ учиться - это делать; лучше всего попробовать его на индивидуальной основе, пока не узнаете, как все работает в Haskell.

Мой ответ довольно прост, но он не подходит для случая, когда один подкласс С++ имеет методы, которые другие не делают. Он выдает ошибку времени выполнения и требует дополнительного кода для загрузки. Вы также должны решить, решает ли модуль "подкласс" ли выбросить ошибку или модуль "суперкласс".