【问题标题】:Polyvariadic functions with polymorphic result value具有多态结果值的多变量函数
【发布时间】:2018-09-04 15:40:17
【问题描述】:

我正在尝试在 Haskell 中将 Pascal 风格的 write 过程实现为多变量函数。这是一个具有单态结果类型的简化版本(在这种情况下为IO),效果很好:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where

import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import System.IO


class WriteParams a where
    writeParams :: IO () -> a

instance (a ~ ()) => WriteParams (IO a) where
    writeParams = id

instance (Show a, WriteParams r) => WriteParams (a -> r) where
    writeParams m a = writeParams (m >> putStr (show a ++ " "))

write :: WriteParams params => params
write = writeParams (return ())

test :: IO ()
test = do
    write 123
    write ('a', 'z') True

但是,当将结果类型更改为多态类型时,为了在具有 MonadIO 实例的不同 monad 中使用该函数,我遇到了重叠或无法确定的实例。具体来说,以前版本中的 a ~ () 技巧不再起作用。最好的方法是以下,但需要大量类型注释:

class WriteParams' m a where
    writeParams' :: m () -> a

instance (MonadIO m, m ~ m') => WriteParams' m (m' ()) where
    writeParams' m = m

instance (MonadIO m, Show a, WriteParams' m r) => WriteParams' m (a -> r) where
    writeParams' m a = writeParams' (m >> liftIO (putStr $ show a ++ " "))

write' :: forall m params . (MonadIO m, WriteParams' m params) => params
write' = writeParams' (return () :: m ())

test' :: IO ()
test' = do
    write' 123 () :: IO ()
    flip runReaderT () $ do
        write' 45 ('a', 'z') :: ReaderT () IO ()
        write' True

有没有办法让这个例子工作而不必到处添加类型注释并仍然保持结果类型多态?

【问题讨论】:

  • 你看过Text.Printf (除非我误解你在这里尝试做的事情,确实是这个吗?)
  • 我怀疑如果你想要良好的类型推断,你将不得不为每个现有的 MonadIO 实例创建一个 WriteParams 实例——或者至少为每个 MonadIO 实例创建一个实例你关心使用。您可以通过提供默认值来减少繁琐,这样您只需要编写例如instance (MonadIO m, a ~ ()) => WriteParams (ReaderT r m a) 并且不需要给 where 块,但是...是的,有点烦人。
  • @Cubic 我的用例比printf 更复杂,我只是在示例中对其进行了一些简化,以便它们看起来相似
  • @DanielWagner 这是一个非常简单但有效的想法,我没有想到。不幸的是,它似乎并没有解决 Li-yao Xia 在他们的回答中提到的test 的泛化问题。
  • 我不确定你的意思。我添加了一个充实我的评论的答案;也许你可以用一个不适用于该解决方案的具体代码 sn-p 来反对。

标签: haskell polyvariadic


【解决方案1】:

这两个实例重叠,因为它们的索引统一:m' () ~ (a -> r)m' ~ (->) a() ~ r

要在m' 不是函数类型时选择第一个实例,您可以添加OVERLAPPING pragma。 (Read more about it in the GHC user guide)

-- We must put the equality (a ~ ()) to the left to make this
-- strictly less specific than (a -> r)
instance (MonadIO m, a ~ ()) => WriteParams (m a) where
    writeParams = liftIO 

instance {-# OVERLAPPING #-} (Show a, WriteParams r) => WriteParams (a -> r) where
    writeParams m a = writeParams (m >> putStr (show a ++ " "))

但是,重叠的实例使得在 monad 是参数 m 的上下文中使用 write 很不方便(尝试概括 test 的签名)。

有一种方法可以通过使用封闭类型族来避免重叠实例,定义一个类型级布尔值,当且仅当给定类型是函数类型时,该布尔值才为真,以便实例可以匹配它。见下文。

可以说它看起来像更多的代码和更复杂的复杂性,但是,除了增加的表现力(我们可以有一个通用的 testMonadIO 约束),我认为这种风格使实例的逻辑更清晰最后通过隔离类型的模式匹配。

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}

module Main where

import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import System.IO


class WriteParams a where
    writeParams :: IO () -> a

instance WriteParamsIf a (IsFun a) => WriteParams a where
    writeParams = writeParamsIf

type family IsFun a :: Bool where
  IsFun (m c) = IsFun1 m
  IsFun a = 'False

type family IsFun1 (f :: * -> *) :: Bool where
  IsFun1 ((->) b) = 'True
  IsFun1 f = 'False

class (isFun ~ IsFun a) => WriteParamsIf a isFun where
  writeParamsIf :: IO () -> a

instance (Show a, WriteParams r) => WriteParamsIf (a -> r) 'True where
  writeParamsIf m a = writeParams (m >> putStr (show a ++ " "))

instance ('False ~ IsFun (m a), MonadIO m, a ~ ()) => WriteParamsIf (m a) 'False where
  writeParamsIf = liftIO

write :: WriteParams params => params
write = writeParams (return ())

test :: (MonadIO m, IsFun1 m ~ 'False) => m ()
test = do
    write 123
    write ('a', 'z') True

main = test  -- for ghc to compile it

关于UndecidableInstances的一些话

不可判定的实例是重叠实例的正交特征,事实上我认为它们的争议要小得多。虽然使用不当OVERLAPPING 可能会导致不连贯性(在不同的上下文中以不同的方式解决约束),但使用不当UndecidableInstances 可能在最坏的情况下将编译器发送到循环中(实际上,一旦达到某个阈值,GHC 就会终止并显示错误消息),这仍然很糟糕,但是当它确实设法解决实例时,仍然可以保证解决方案是唯一的。

UndecidableInstances 解除了很久以前有意义的限制,但现在限制太多,无法使用类型类的现代扩展。

在实践中,大多数用UndecidableInstances 定义的常见类型类和实例,包括上面的那个,仍然保证它们的解析将终止。事实上,there is an active proposal 是一个新的实例终止检查器。 (我还不知道它是否在这里处理这种情况。)

【讨论】:

  • 谢谢,这似乎正是我想要的。让我有点害怕的一件事是使用不可判定的实例。我已经尝试避免重叠实例,因为有人告诉我它们可能导致不一致的行为。不可判定的情况不是更糟吗?或者它们在与封闭类型家族一起使用时是否可以(我以前从未使用过后者)?
  • 我在回答中添加了一些关于不可判定实例的内容。简短的回答是,它们比重叠实例更安全,因为它们不会破坏类型类的一致性。
  • 重叠实例在它们都定义在同一个模块中时也没有问题,就像这里的情况一样。只是通过语言中的适当机制来限制它们的使用可能会更好地接受它们。
【解决方案2】:

在这里,我将我的评论充实为一个答案。我们将保留您原始类的想法,甚至现有实例,仅添加实例。只需为每个现有的MonadIO 实例添加一个实例;我将只做一个来说明这种模式。

instance (MonadIO m, a ~ ()) => WriteParams (ReaderT r m a) where
    writeParams = liftIO

一切正常:

main = do
    write 45
    flip runReaderT () $ do
        write 45 ('a', 'z')
        write "hi"

这会在执行时打印45 45 ('a','z') "hi"

如果您想稍微减少 writeParams = liftIO 样板,可以打开 DefaultSignatures 并添加:

class WriteParams a where
    writeParams :: IO () -> a
    default writeParams :: (MonadIO m, a ~ m ()) => IO () -> a
    writeParams = liftIO

那么IOReaderT 实例就是:

instance a ~ () => WriteParams (IO a)
instance (MonadIO m, a ~ ()) => WriteParams (ReaderT r m a)

【讨论】:

  • "This prints ... when executed." 中 ... 后面有一个尾随空格。 Markdown 解析器似乎正在吃的句子。如果您知道如何让它出现,请告诉我,我将编辑答案以使其更准确。
  • 我在之前的评论中的意思是夏立耀的想法是将test 的类型泛化为(MonadIO m, ...) => m () 之类的东西,这很好,但似乎不可能版本
猜你喜欢
  • 1970-01-01
  • 2015-04-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-21
相关资源
最近更新 更多