Наследование для расширения структуры данных в 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 с записями или типами классов или что-то еще.
Спасибо.
Ответы
Ответ 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.
Мой ответ довольно прост, но он не подходит для случая, когда один подкласс С++ имеет методы, которые другие не делают. Он выдает ошибку времени выполнения и требует дополнительного кода для загрузки. Вы также должны решить, решает ли модуль "подкласс" ли выбросить ошибку или модуль "суперкласс".