在Haskell中是否存在“对象平等”感?

时间:2015-01-04 04:39:44

标签: haskell identity equality

如果我在Haskell中有一个单独的链表:

data LL a = Empty | Node a (LL a) deriving (Show, Eq)

我可以轻松实现在结尾和开头插入的方法。但是在特定元素之后或之前插入呢?如果我有LL Integer,我是否可以在包含4的特定节点之后插入1而不是第一个1之间在Haskell中进行区分它在处理列表时会看到什么?

Node 1 (Node 2 (Node 3 (Node 1 Empty)))

我很好奇insertAfter方法看起来如何能够指定“在包含1的特定节点后插入5”。如果我想在包含1的第一个节点之后插入,我是否必须传入整个列表来指定这个,而对于最后一个节点,只需要Node 1 Empty

我不确定将此作为'对象相等'来解决是否正确 - 但我想知道是否有一种方法可以在这样的数据结构中引用具有相同有效负载的类型的特定元素。

4 个答案:

答案 0 :(得分:6)

不,没有这样的事情。分辨价值的唯一方法是它们的结构;某些语言中的对象没有 identity 。也就是说,你无法区分这两个值:(Just 5, Just 5)的行为与let x = Just 5 in (x, x)完全相同。同样,“此Node 1”与“其他Node 1”之间没有区别:它们无法区分。

通常,这个问题的“解决方案”是以其他方式考虑您的问题,以便不再需要基于身份进行区分(通常实际上并不需要)。但是,正如评论中所提到的,您可以通过生成某种类型的不同标记来模拟其他语言的“指针”机制,例如增加整数,并为每个对象分配一个,以便您可以区分它们。

答案 1 :(得分:2)

正如其他人指出的那样,在Haskell中,每个值都是不可变的,没有对象。 要指定唯一节点,您需要以结构方式指定它(例如,链接列表中包含1的第一个节点)或者以某种方式为每个节点提供额外的标记(模拟在命令性世界中发生的事情),以便我们可以区分它们。

为了在结构上区分节点与其他节点,我们基本上需要知道其位置 该节点,例如一个zipper不仅可以为您提供该点的价值,还可以为您提供“邻域”。

更详细地说“为每个节点提供额外的标签”:

首先,您需要将每个值都设为对象,这需要您在运行时生成唯一标记。这通常由一个分配器完成,最简单的分配器可能只保留一个整数,当我们需要创建一个新对象时它会突然出现:

-- | bumps counter
genId :: (Monad m, Functor m, Enum e) => StateT e m e
genId = get <* modify succ

-- | given a value, initializes a new node value
newNode :: (Monad m, Functor m, Enum e) => a -> StateT e m (a,e)
newNode x = genId >>= return . (x,)

如果你想让一个现有的链表工作,我们需要遍历它并给每个节点值一个标签,使其成为一个对象:

-- | tags the llnked list with an extra value
tagged :: (Traversable f, Enum e, Monad m, Functor m)
       => f a -> StateT e m (f (a,e))
tagged = traverse newNode

这是完整的演示,看起来很Maybe "a little"尴尬:

{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable, TupleSections #-}
import Control.Applicative
import Control.Monad.State hiding (mapM_)
import Data.Traversable
import Data.Foldable
import Prelude hiding (mapM_)

data LL a = Empty | Node a (LL a)
    deriving (Show, Eq, Functor, Foldable, Traversable)

-- | bumps counter
genId :: (Monad m, Functor m, Enum e) => StateT e m e
genId = get <* modify succ

-- | given a value, initializes a new node value
newNode :: (Monad m, Functor m, Enum e) => a -> StateT e m (a,e)
newNode x = genId >>= return . (x,)

example :: LL Int
example = Node 1 (Node 2 (Node 3 (Node 1 Empty)))

-- | tags the llnked list with an extra value
tagged :: (Traversable f, Enum e, Monad m, Functor m)
       => f a -> StateT e m (f (a,e))
tagged = traverse newNode

insertAfter :: (a -> Bool) -> a -> LL a -> LL a
insertAfter cond e ll = case ll of
    Empty -> Empty
    Node v vs -> Node v (if cond v
                           then Node e vs
                           else insertAfter cond e vs)

demo :: StateT Int IO ()
demo = do
    -- ll1 = Node (1,0) (Node (2,1) (Node (3,2) (Node (1,3) Empty)))
    ll1 <- tagged example
    nd <- newNode 10
    let tagIs t = (== t) . snd
        ll2 = insertAfter (tagIs 0) nd ll1
        -- ll2 = Node (1,0) (Node (10,4) (Node (2,1) (Node (3,2) (Node (1,3) Empty))))
        ll3 = insertAfter (tagIs 3) nd ll1
        -- ll3 = Node (1,0) (Node (2,1) (Node (3,2) (Node (1,3) (Node (10,4) Empty))))
    liftIO $ mapM_ print [ll1,ll2,ll3]

main :: IO ()
main = evalStateT demo (0 :: Int)

在这个演示中,tagIs基本上是在做“对象相等”的事情,因为它只对我们之前添加的额外标记感兴趣。请注意,我在这里作弊是为了指定两个节点,其“值”为1:一个标记为0,另一个标记为3。在运行程序之前,无法确定实际标记是什么。 (就像硬编码指针值并希望它恰好工作一样)在更现实的设置中,您需要另一个函数来扫描链表并收集具有特定值的标签列表(在此示例中,如果您搜索链接列表以查找具有“值”1的所有节点,您可以使用[0,3])。

“对象平等”似乎更像是命令式编程语言的概念,它假设有分配器提供“引用”或“指针”,以便我们可以谈论“对象平等”。我们必须模拟那个分配器,我想这是使函数式编程处理它有点尴尬的事情。

答案 2 :(得分:2)

Kristopher Micinski remarked你实际上可以用ST monad做类似的事情,你也可以用IO来做。具体来说,您可以创建STRefIORef,这是一种可变的框。只能使用IOST操作来访问该框,这样可以保持“纯”和“不纯”代码之间的清晰分离。这些引用具有标识 - 如果两个相等则告诉您它们是否实际上是相同的框,而不是它们是否具有相同的内容。但这并不是那么令人愉快,而且如果没有充分的理由,你不可能做的事情。

答案 3 :(得分:1)

不,因为它会破坏引用透明度。多次调用具有相同输入的方法的结果应该是无法区分的,并且应该可以通过使用该输入调用方法一次透明地替换它,然后重新使用结果。但是,调用多次返回某个结构的方法可能每次都会生成一个新的结构副本 - 具有不同&#34; identity&#34;的结构。如果你能以某种方式告诉他们他们有不同的身份,那么它就违反了参照透明度。