2017-08-12 34 views
5

我有一個使用記錄語法的Haskell類型。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

回答

5

隱藏構造函數,然後爲每個字段定義新的存取函數是一種解決方案,但對於包含大量字段的記錄而言,它可能很乏味。

下面是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來自客戶的構造函數。