Haskell 用于设置索引结构化值集合的习惯用法

Haskell idiom for setting up a collection of indexed structured values

我收集了大约十几个我定义的类型的结构化事物(比如 Component),每个都可以用 "name" 标识,并且正在努力排序出一种惯用的 Haskell 方式来实例化和检索它们。这些东西在我的应用程序中经常使用,因此从概念上讲,它们是一组全局常量或一组 table 常量,理想情况下会被初始化并保存以供快速检索。

我不喜欢table 我目前的方法,只是使用一个函数来 "compute" 每个 Component 从它的名字。

data Component = Component {
                    someData :: !String,
                    otherData :: ![Int] 
                } deriving Show

component :: Name -> Component
component n = case n of
    -- about a dozen in the application
    "1"       -> Component "lasdkfj;alksdjfalkf" [1]
    "Q"       -> Component "nvjufhhqwe" [5,10,11]
    "other"   -> Component "ugugugu" []
    "A"       -> Component "alkkjsfkjaleifuhqiweufjc" []
    "B"       -> Component "randomletters" []
    "C"       -> Component "nothingimportant" [9,10]
    "b"       -> Component "uk" []
    "c"       -> Component "x" [4,2,7,9,0]
    ""        -> Component "ABC" []
    -- if not listed above, the Component is computed
    otherwise -> Component (someFunctionOf n) (someOtherFunctionOf n)

我觉得这不对。一方面,Component 的名称实际上是 Component 的一部分,但不包含在类型中。更重要的是,即使是常量值也会被计算出来,而实际上它们应该只在 table.

中初始化

考虑到这一点,我也尝试过

type Name = String

import Data.Maybe
import Data.Map

data Component = Component {
                    name :: Name,
                    someDate :: String,
                    otherData :: [Int] 
                } deriving Show

components = fromList $ (\c -> (name c, c)) <$> [
    Component "1" "lasdkfj;alksdjfalkf" [1],
    Component "Q" "nvjufhhqwe" [5,10,11],
    Component "other" "ugugugu" [],
    Component "A" "alkkjsfkjaleifuhqiweufjc" [],
    Component "B" "randomletters" [],
    Component "C" "nothingimportant" [9,10],
    Component "b" "uk" [],
    Component "c" "x" [4,2,7,9,0],
    Component "" "ABC" []
    ]

component :: Name -> Component
component n | isNothing c = Component n (someFunctionOf n) (someOtherFunctionOf n) 
            | otherwise   = fromJust c  
        where c = Data.Map.lookup n components

这样做的好处是可以清楚地将 "constant" 值视为常量,但感觉很尴尬,因为它引入了一个中间值(Map components)并在那里复制了名称(在Component中作为对应的key)。

无论如何,我觉得我做的这一切都是错误的,并且必须有更好的方法来设置一组索引的结构化值,其中包括一堆常量和计算值。

你基于 Map 的解决方案对我来说看起来不错。两个小调整:首先,您应该对 Data.Map 进行合格的导入,以避免名称冲突:

import qualified Data.Map as M
import Data.Map (Map)

第二个 import 只是为了方便起见。有了它,你就不需要在类型签名中写M.Map

其次,isNothingisJust 不是很地道。使用 maybefromMaybe 或简单地对 Maybe 值进行模式匹配会更清楚。作为奖励,如果你这样做,你不需要使用 fromJust(尽可能避免使用,因为它是部分的)。

component :: Name -> Component
component n = fromMaybe
    (Component n (someFunctionOf n) (someOtherFunctionOf n))
    (M.lookup n components)

but feels awkward, since it introduces an intermediate value (the Map components)

我知道,但引入中间值确实没有错。这样做可以让代码更容易理解,让每个部分的作用更清晰,并且更容易重用。如果您在其他任何地方都不需要 components 映射(很可能是这种情况)并且不想为它做一个顶级定义,只需将它放在 where 子句中即可。

and duplicates the name there

这是一个烦恼,虽然是一个相对较小的烦恼。如果您的代码的用户无法访问 components 字典,他们就无法通过更改存储组件的名称来引入错误;只有你可以这样做。尽管如此,如果您想尽量减少可能引入错误的地方的数量(这本身就是一个合理的目标),您可以将 components 的类型更改为...

components :: Map Name ComponentData

... 其中 ComponentData 是您对 Component 的原始无名定义。 component 函数是用户实际看到的函数,可以保留其当前类型:只需引入类似...

的内容
giveNameToComponent :: Name -> ComponentData -> Component

...并将其定义更改为...

component :: Name -> Component
component n = fromMaybe
    (Component n (someFunctionOf n) (someOtherFunctionOf n))
    (giveNameToComponent n <$> M.lookup n components)

... 或者,等效地使用 maybe:

component :: Name -> Component
component n = maybe
    (Component n (someFunctionOf n) (someOtherFunctionOf n))
    (giveNameToComponent n)
    (M.lookup n components)