Haskell泛型实现千/兆/吉单位转千单位的通用函数
Great question! It's smart to notice that convertToKB and convertToKBs share identical logic—we can absolutely use GHC Generics to abstract this into a single, reusable convertToK function. The key is to inspect the constructor names of your types (via their generic representation) and apply the correct scaling factor based on the K/M/G prefix.
Let's walk through fixing those three "?" spots and building the generic solution step by step:
Step 1: Adjust the ConvertToK' Class
First, we need to modify the ConvertToK' class to accept a scaling factor parameter. This lets us pass the correct multiplier (1, 1000, 1000000) down to the code that modifies the underlying Double value:
class ConvertToK' f where convertToK' :: Double -> f p -> f p
The Double here is our scaling factor, and f p is the generic representation of our type.
Step 2: Implement ConvertToK' Instances
We need instances for all the generic building blocks used by your types:
Empty Constructors (
U1):
Our types don't use empty constructors, but we need this instance to satisfy generic constraints:instance ConvertToK' U1 where convertToK' _ U1 = U1Wrapped Values (
K1):
This handles the actualDoublevalue inside each constructor. We multiply it by the passed-in factor:instance ConvertToK' (K1 i Double) where convertToK' factor (K1 d) = K1 (d * factor)Constructors (
C1):
This is where we inspect the constructor name to determine the scaling factor. Using theConstructorconstraint, we can get the constructor's name, check its first character, and pass the right factor to process the constructor's value:instance (Constructor c, ConvertToK' f) => ConvertToK' (C1 c f) where convertToK' _ c1@(M1 x) = let conPrefix = take 1 (conName c1) factor = case conPrefix of "K" -> 1.0 "M" -> 1000.0 "G" -> 1000000.0 _ -> 1.0 -- Fallback for unexpected constructor names in M1 (convertToK' factor x)Sum Types (
:+:):
This handles the multiple constructors in yourDataAmountandSpeedtypes. We just pass the factor through to each branch and wrap the result back up:instance (ConvertToK' f, ConvertToK' g) => ConvertToK' (f :+: g) where convertToK' factor (L1 x) = L1 (convertToK' factor x) convertToK' factor (R1 x) = R1 (convertToK' factor x)
Step 3: Add Default Generic Implementation to ConvertToK
Now we update the ConvertToK class to use our generic logic by default. We use from to convert the original type to its generic representation, process it with convertToK', then use to to convert back:
class ConvertToK a where convertToK :: a -> a default convertToK :: (Generic a, ConvertToK' (Rep a)) => a -> a convertToK x = to (convertToK' 1.0 (from x))
Step 4: Hook Up Your Types
Since DataAmount and Speed already derive Generic, we just need to declare instances for ConvertToK—the default implementation will handle the rest:
instance ConvertToK DataAmount instance ConvertToK Speed
Full Working Code
Here's the complete code with all these changes, replacing your duplicate conversion functions with the generic convertToK:
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE TypeOperators #-} module Lib ( someFunc ) where import GHC.Generics import GHC.Generics (Constructor(..)) data DataAmount = KB Double | MB Double | GB Double deriving (Generic, Show) data Speed = KBs Double | MBs Double | GBs Double deriving (Generic, Show) class ConvertToK a where convertToK :: a -> a default convertToK :: (Generic a, ConvertToK' (Rep a)) => a -> a convertToK x = to (convertToK' 1.0 (from x)) class ConvertToK' f where convertToK' :: Double -> f p -> f p instance ConvertToK' U1 where convertToK' _ U1 = U1 instance ConvertToK' (K1 i Double) where convertToK' factor (K1 d) = K1 (d * factor) instance (Constructor c, ConvertToK' f) => ConvertToK' (C1 c f) where convertToK' _ c1@(M1 x) = let conPrefix = take 1 (conName c1) factor = case conPrefix of "K" -> 1.0 "M" -> 1000.0 "G" -> 1000000.0 _ -> 1.0 in M1 (convertToK' factor x) instance (ConvertToK' f, ConvertToK' g) => ConvertToK' (f :+: g) where convertToK' factor (L1 x) = L1 (convertToK' factor x) convertToK' factor (R1 x) = R1 (convertToK' factor x) instance ConvertToK DataAmount instance ConvertToK Speed timeDiv (KB x) (KBs z) | z>0 = x/z someFunc :: IO () someFunc = do putStrLn "Give the amount of data:" dat <- readLn putStrLn "Give 1 for KB 2 for MB 3 for GB:" unit <- readLn let dataAmount = case unit of 1 -> KB dat 2 -> MB dat 3 -> GB dat _ -> KB dat putStrLn "Give speed of data:" speed <- readLn putStrLn "Give 1 for KB/s 2 for MB/s 3 for GB/s:" speedunit <- readLn let speedAmount = case speedunit of 1 -> KBs speed 2 -> MBs speed 3 -> GBs speed _ -> KBs speed -- Use our generic convertToK instead of duplicate functions let speedAmountKBs = convertToK speedAmount let dataAmountKB = convertToK dataAmount let result = timeDiv dataAmountKB speedAmountKBs putStrLn $ "You need " ++ show result ++ " seconds"
Key Notes
- The
Constructorconstraint gives us access toconName, which lets us get the name of each constructor (like "KB" or "MBs"). - We use the first character of the constructor name to decide the scaling factor—this works perfectly for your naming pattern.
- Now you can reuse
convertToKfor any future types that follow the same K/M/G prefix pattern, just by derivingGenericand declaring aConvertToKinstance.
内容的提问来源于stack exchange,提问作者Dragno

