Haskell 只读导出记录访问的实现

10

我有一个使用记录语法的Haskell类型。

data Foo a = Foo { getDims :: (Int, Int), getData :: [a] }

我不想导出Foo值构造函数,这样用户就无法构建无效的对象。但是,我想导出getDims,以便用户可以获取数据结构的维度。如果我这样做

module Data.ModuleName(Foo(getDims)) where

用户可以使用getDims来获取尺寸,但问题在于他们也可以使用记录更新语法来更新字段。

getDims foo -- This is allowed (as intended)
foo { getDims = (999, 999) } -- But this is also allowed (not intended)

我希望避免后一种情况,因为这将使数据处于无效状态。我意识到可以简单地不使用记录。

data Foo a = Foo { getDims_ :: (Int, Int), getData :: [a] }

getDims :: Foo a -> (Int, Int)
getDims = getDims_

但这似乎是绕过问题的一种比较迂回的方式。有没有一种方法可以在仅导出记录名称以进行读取访问而不进行写入访问的情况下继续使用记录语法?


3
没有。需要注意的是,你后面的例子是实现这一目标的标准方式,并且仍然使用了记录。你所说的“绕路”实际上只是额外的两行代码。 - user2407038
1个回答

6
隐藏构造函数,然后为每个字段定义新的访问器函数是一种解决方案,但对于具有大量字段的记录来说可能会变得繁琐。
在 GHC 8.2.1 中,使用新的 HasField 类型类可以避免为每个字段定义函数。具体方法是定义一个辅助的 newtype,如下所示:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}    
{-# LANGUAGE PolyKinds #-} -- Important, obscure errors happen without this.

import GHC.Records (HasField(..))

-- Do NOT export the actual constructor!
newtype Moat r = Moat r

-- Export this instead.
moat :: r -> Moat r
moat = Moat

-- If r has a field, Moat r also has that field
instance HasField s r v => HasField s (Moat r) v where
    getField (Moat r) = getField @s r

每个记录r中的字段都可以通过以下语法从Moat r访问:

λ :set -XDataKinds
λ :set -XTypeApplications
λ getField @"getDims" $ moat (Foo (5,5) ['s'])
(5,5)
Foo 构造函数应该对客户端隐藏。然而,Foo 的字段访问器不应该被隐藏;它们必须在 MoatHasField 实例的范围内才能启动。
您公共 API 中的每个函数都应该返回和接收 Moat Foo 而不是 Foo
为了使访问器语法稍微简洁一些,我们可以转向 OverloadedLabels
import GHC.OverloadedLabels

newtype Label r v = Label { field :: r -> v }

instance HasField l r v => IsLabel l (Label r v)  where
    fromLabel = Label (getField @l)

在ghci中:
λ :set -XOverloadedLabels
λ field #getDims $ moat (Foo (5,5) ['s'])
(5,5)

不必隐藏Foo构造函数,另一个选择是将Foo完全公开,并在库中定义Moat,从客户端隐藏任何Moat构造函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接