首页 > 解决方案 > 以正确的方式在 Haskell 中编写模块

问题描述

(我完全重写了这个问题,以便更好地关注它;如果您想查看原件,可以查看更改的历史。)

假设我有两个模块:

module Module1 (inverseAndSqrt) where

type TwoOpts a = (Maybe a, Maybe a)

inverseAndSqrt :: Int -> TwoOpts Float
inverseAndSqrt x = (if x /= 0 then Just (1.0/(fromIntegral x)) else Nothing,
                    if x >= 0 then Just (sqrt $ fromIntegral x) else Nothing)
module Module2 where

import Module1

fun :: (Maybe Float, Maybe Float) -> Float
fun (Just x, Just y) = x + y
fun (Just x, Nothing) = x
fun (Nothing, Just y) = y

exportedFun :: Int -> Float
exportedFun = fun . inverseAndSqrt

我想从设计原则的角度理解的是:我应该如何Module1与其他模块(例如Module2)进行接口,使其能够很好地封装、可重用等?

我看到的问题是

我应该如何设计Module1(并因此Module2也进行编辑)以使两者不紧密耦合?

我能想到的一件事是,也许我应该定义一个类型来表达“一个里面有两个可选东西的盒子”是什么,class然后将它用作一个通用接口。但这应该在两个模块中吗?在他们中的任何一个?或者它们都没有,在第三个模块中?或者也许不需要这样的 /concept?Module1Module2class

我不是计算机科学家,所以我敢肯定,由于缺乏经验理论背景,这个问题凸显了我对我的一些误解。欢迎任何帮助填补空白。

我想支持的可能修改

标签: haskellfunctional-programmingseparation-of-concernsdecouplingdesign-principles

解决方案


对我来说,Haskell 设计都是以类型为中心的。函数的设计规则只是“使用最通用和最准确的类型来完成这项工作”,而 Haskell 中的整个设计问题就是为这项工作提出最佳类型。

我们希望类型中没有“垃圾”,以便它们对您要表示的每个值都有一个表示。例如String,对于数字来说是一个不好的表示,因为"0", "0.0", "-0"它们都意味着相同的东西,而且因为"The Prisoner"它不是一个数字——它是一个没有有效外延的有效表示。如果出于性能原因,可以以多种方式表示相同的表示,则类型的 API 应该使用户看不到这种差异。

所以在你的情况下,(Maybe a, Maybe a)是完美的——这正是你需要的意思。使用更复杂的东西是不必要的,只会使用户的事情复杂化。在某些时候,您公开的任何内容都必须可转换为Maybe a用于第一件事的 a 和Maybe a用于第二件事的 a,并且没有额外的信息,因此元组是完美的。是否使用类型同义词是一种风格问题——我根本不喜欢使用同义词,并且只在我想到更正式的抽象时才给出类型名称。

内涵很重要。例如,如果我有一个查找二次多项式根的函数,我可能不会使用TwoOpts,即使最多有两个。从直觉上看,我的返回值都是“同一种东西”,这一事实让我更喜欢一个列表(或者如果我感觉特别挑剔,一个SetBag),即使列表最多有两个元素。我只是让它符合我当时对领域的最佳理解,所以我不会改变它,除非我对领域的理解发生了重大变化,在这种情况下,审查其所有用途的机会正是我想要的. 如果您正在编写尽可能多态的函数,那么您通常不需要更改任何内容,但使用含义的特定时刻,需要领域知识的确切时刻(例如理解和之间的关系TwoOptsSet。如果它由足够灵活的多态材料制成,则无需“重做管道”。

假设您没有与标准类型类似的干净同构(Maybe a, Maybe a),并且您想形式化TwoOpts. 这里的方法是从它的构造函数、组合器和消除器中构建一个 API。例如:

data TwoOpts a    -- abstract, not exposed

-- constructors 
none :: TwoOpts a
justLeft :: a -> TwoOpts a
justRight :: a -> TwoOpts a
both :: a -> a -> TwoOpts a

-- combinators
-- Semigroup and Monoid at least
swap :: TwoOpts a -> TwoOpts a

-- eliminators
getLeft :: TwoOpts a -> Maybe a
getRight :: TwoOpts a -> Maybe a

在这种情况下,消除器准确地将您的表示(Maybe a, Maybe a)作为它们的最终代数。

-- same as the tuple in a newtype, just more conventional
data TwoOpts a = TwoOpts (Maybe a) (Maybe a)

或者,如果您想专注于构造函数方面,您可以使用初始代数

data TwoOpts a
    = None
    | JustLeft a
    | JustRight a
    | Both a a

只要它仍然实现了上面的组合 API,你就可以自由地改变这个表示。如果您有理由使用相同 API 的不同表示,请将 API 制作为类型类(类型类设计完全是另一回事)。

用爱因斯坦的话来说,“让它尽可能简单,但不要更简单”。


推荐阅读