我需要帮助将OO概念转换为Haskell。
想象一下Vehicle
类以及Car
和Truck
子类,使用driveOneMile
方法返回表示所用燃油总量的双精度型。每次拨打driveOneMile
也会改变车辆的内部状态。
这是我迄今为止在Haskell中所做的事情(因为在Haskell中没有实例变量,似乎我必须创建自己的"状态"类型):
type CarState = (Double,Double)
initialCarState = (0,0)
driveCarOneMile :: CarState -> (Double,CarState) --Double: total fuel used
driveCarOneMile s = ...
--the internal state of trucks is more complex. needs three Doubles
type TruckState = (Double,Double,Double)
initialTruckState = (0,0,0)
driveTruckOneMile :: TruckState -> (Double,TruckState)
driveTruckOneMile s = ...
以类似的方式,我可以建造其他车辆和他们的驾驶"功能
这就是我开车两次的方式:
fuelFor2Miles = fst $ driveCarOneMile $ snd $ driveCarOneMile initialCarState
答案 0 :(得分:8)
Haskell中的类与OO类完全不同,并且使用它们就像在大多数情况下编写OO代码一样,使事情变得比它们应该更复杂。特别是,一旦你开始考虑" [采取]许多汽车,卡车和其他车辆的集合"在OO术语中,你直接沿着兔子洞(正如Haskell Antipattern: Existential Typeclass所指出的那样)。
您可能根本不需要上课来模拟不同类型的车辆。你可以定义
data Vehicle = Car CarState | Truck TruckState
然后
driveOneMile :: Vehicle -> (Double, Vehicle)
使用模式匹配来区分不同的车辆。
即使您真的需要更像类的东西(例如,您正在编写图书馆并希望用户提供自己的车辆),这并不一定意味着您需要异构集合。您可以为Vehicle
提供一个构造函数,并为其添加与类方法相对应的字段,以便该类成为一个类型,并且类实例成为值(这是反模式文章所倡导的方法)。您可能还有一个Vehicle
类,其toGeneralVehicle :: a -> GeneralVehicle
方法,并且具有根据GeneralVehicle
类型定义的常见行为。
P.S。:TruckState -> (Double,TruckState)
等签名背后的想法是合理的。事实上,我说你不小心发现了State
类型! State
只是一个方便的抽象,使状态传递管道隐含。如果您感到好奇,请查找有关State
monad的问题和教程。
答案 1 :(得分:2)
我对你的问题的最初直觉反应是使用状态monad实现这一点,duplode在他的回答中提示。这是一种在代码有些必要的地方完成你想要的东西的方法。
正如duplode建议的那样,我也会定义
data Vehicle = Car CarState | Truck TruckState
然后,您可以将driveOneMile
定义为
driveOneMile :: State Vehicle Double
driveOneMile = do
vehicleState <- get
case vehicleState of
Car carState -> do
...
return totalFuelUsed
Truck truckState -> do
...
return totalFuelUsed
现在,在你的例子中,你驾驶两英里,难道你不想总结每英里使用的燃料量吗?你拥有它的方式,它只返回第二英里使用的燃料量。
使用州monad,driveTwoMile
甚至driveNMiles n
非常简单:
driveTwoMile :: State Vehicle Double
driveTwoMile = do
firstMileFuelUsage <- driveOneMile
secondMileFuelUsage <- driveOneMile
return (firstMileFuelUsage + secondMileFuelUsage)
driveNMiles :: Int -> State Vehicle Double
driveNMiles n = do
fuelUsages <- replicateM n driveOneMile
return (sum fuelUsages)
至于车辆列表,我不知道最好的方法是什么,但以下是这样做的一种方式(我在这里使用列表作为monad):
type DrivingDistance = Int
driveManyVehicles :: [Vehicle] -> [DrivingDistance] -> [(Double, Vehicle)]
driveManyVehicles v d = do
(currentVehicle, drivingDistance) <- zip v d
return (runState (driveNMiles drivingDistance) currentVehicle)
答案 2 :(得分:0)
如果你真的想要一堂课,那就想这样:
class Vehicle a where
driveOneMile :: a -> Double
data Truck = Truck -- Fill in the details...
data Car = Car -- Fill in the details...
instance Vehicle Truck
instance Vehicle Car
但正如前面回答的作者所说的那样 - 将它们作为Vehicle数据类型的不同构造函数实现可能更好。
答案 3 :(得分:0)
这是我为你的场景建模的方式:
我不会把汽车和卡车放在一起,以适应一种总和类型。求和类型并不是为了描述类型之间数据的某些共性(而是一种不相交的案例联合)您可以通过类型类捕获数据的通用性(尽管它不会为两种类型节省很多)。然后我会将您的车辆集合定义为每种类型的列表。
type Miles = Double
type Gallons = Double
data Truck = Truck { tid :: Int, truckTankGallons :: Gallons, tmpg :: Double } deriving (Show)
data Car = Car { cid :: Int, carTankGallons :: Gallons, cmpg :: Double } deriving (Show)
class GasVehicle a where
mpg :: a -> Double
gasGallons :: a -> Gallons
changeGasAmt :: Gallons -> a -> a
instance GasVehicle Car where
mpg = cmpg
gasGallons = carTankGallons
changeGasAmt g c = Car (cid c) g (cmpg c)
instance GasVehicle Truck where
mpg = tmpg
gasGallons = truckTankGallons
changeGasAmt g c = Truck (tid c) g (tmpg c)
milesToGallons :: GasVehicle a => a -> Miles -> Gallons
milesToGallons a m = m / (mpg a)
driveMiles :: GasVehicle a => Miles -> a -> (a, Gallons)
driveMiles m a = (a', dg) where
dg = (milesToGallons a m)
a' = changeGasAmt (gasGallons a - dg) a
data Vehicles = Vehicles { cars :: [Car], trucks :: [Truck] } deriving (Show)
driveVehicles :: Miles -> Vehicles -> (Vehicles, Gallons)
driveVehicles m v = (Vehicles cars' trucks', gasUsed) where
gasUsed = (sum carGallons) + (sum truckGallons)
(cars', carGallons) = unzip $ map (driveMiles m) $ cars v
(trucks', truckGallons) = unzip $ map (driveMiles m) $ trucks v