Haskell 中面向对象抽象类型的模式

Pattern for OO abstract types in Haskell

我来自 OO 语言,目前正在尝试使用 Haskell 开发应用程序。 我正在尝试使用 OO 语言中的抽象 class,但这似乎不太适合 Haskell(可能还有大多数函数式语言)类型系统。

比方说 Java,我要表达的内容的最小代码可能看起来像这样

abstract class Transformation {
    public abstract void transform(Image image);
}

class ResizeTransformation extends Transformation {
    public void transform(Image image) {
        // resize the image
    }
}

// some other classes extending Transformation

class Worker {
    public void applyTransformations(Image image, Transformation[] trs) {
        for (Transformation t: trs) {
            t.transform(image);
        }
    }
}

class Parser {
    public Transformation parseTransformation(String rawTransformation) {
        // return ResizeTransformation or some other concrete transformation
    }
}

我试图在 Haskell 中用 Transformation class 表达这一点,并为每个转换创建一些实例,但我被解析器卡住了,因为它似乎没有可以 return 抽象类型。

我的 Haskell 代码大致如下所示:

class Transformation a where
  transform :: Image -> a -> Image

data Resize = Resize
    { newHeight :: Int
    , newWidth  :: Int
    } deriving (Show)


instance Transformation Resize where
    transform image resize = -- resize the image

applyTransformations :: (Transformation a) => Image -> [a] -> Image
applyTransformations image transformations = foldl transform image transformations

我想要一个带有这样签名的函数

parseTransformation :: (Transformation a) => String -> Maybe a

但我认为这是不可能的,因为 returning Maybe Resize 结果是 could not deduce a ~ Resize,我可以理解。

我是不是遗漏了什么,或者我想在 Haskell 中做一个反模式。 如果这似乎是一种糟糕的方法,我很乐意在 Haskell.

中提示处理这种情况的更好方法。

感谢评论,我设法让应用程序正常工作, 所以我会回答我的问题,描述我是如何进行的。

免责声明:第一次写真实的 Haskell 中的 world 应用程序,所以我不确定所有这些都是最佳实践。

单一方法class/interface

正如我在问题的评论中所说,我使用了一个函数来实现 class 或 Java 中的接口。

abstract class Transformation {
    public abstract void transform(Image image);
}

但是,考虑到我还需要有关转换的信息, 我使用了一条记录并在其中包含了一个函数,所以而不是

class Transformation a where
  transform :: Image -> a -> Image

data Resize = Resize
    { newHeight :: Int
    , newWidth  :: Int
    } deriving (Show)

instance Transformation Resize where
    transform image resize = -- resize the image

我最终得到了类似

的结果
type Processor = Image -> Image
data Transformation = Transformation
                      { name      :: String
                      , args      :: String
                      , processor :: Processor
                      }

parseTransformation :: String -> String -> Maybe Transformation
parseProcessor      :: String -> String -> Maybe Processor

resizeProcessor :: ResizeInfo -> Processor
cropProcessor   :: CropInfo -> Processor

我根据 parseProcessor 解析了 ResizeInfoCropInfo 给定的转换名称。 所以在解析了所有的转换之后,我得到了一个 Transformation 的列表 处理图像和保存一些信息所需的一切 转换。

注入接口

我得到的第二个问题是 Java 中 DI 可以解决什么问题。 这是一个简化的例子:

public interface Storage {
    public void load(StoredObject o);
    public void save(StoredObject o);
}

public class S3Storage implements Storage {
    // some implementation
}

public class LocalStorage implements Storage {
    // some implementation
}

有了这个定义,我可以简单地实例化存储 取决于设置文件或其他文件,并将其注入 在我的申请的其余部分。

好吧,这实际上与我在 Haskell 中所做的并没有太大不同, 而且效果很好。

我使用了这样定义的存储class

data StoredObject = StoredObject
                  { uniqueId      :: String
                  , content       :: Maybe ByteString
                  } deriving (Show)

class Storage a where
    load :: a -> StoredObject -> IO (Either StorageError StoredObject)
    save :: a -> StoredObject -> IO (Either StorageError ())

然后简单地创建了 LocalStorageS3Storage 并为每个写了一个 Storage 实例。

data LocalStorage = LocalStorage
                  { baseDir :: String
                  }
instance Storage LocalStorage where
    save = -- implementation
    load = -- implementation

我用这样的东西更改了我用来抽象已用存储的函数

downloadHandler :: (Storage a) => a -> IO ()
downloadHandler storage = -- process download

最后我简单解析了加载应用时的配置, 基于它创建了我想要的存储,并传递了它。 大致是这样的。

runApp :: String -> IO ()
runApp "local" = run makeLocalStorage
runApp "s3"    = run makeS3Storage
runApp storage = putStrLn ("no storage named " ++ storage) >> exitFailure

run :: (Storage a) => a -> IO ()
run storage = -- run with my storage

通过上面的两个模式,我现在设法让事情变干并且具有足够的可扩展性。