【问题标题】:Haskell type of specific data constructorHaskell 类型的特定数据构造函数
【发布时间】:2015-11-19 20:47:18
【问题描述】:

假设我有以下 Haskell 代码:

data Option
    = Help
    | Opt1 Int Double String
    -- more options would be here in a real case

handleOption :: Option -> IO ()
handleOption option = case option of
    Help -> handleHelp
    Opt1 n f s -> handleOpt1 n f s

handleHelp :: IO ()
handleHelp = print "help"

handleOpt1 :: Int -> Double -> String -> IO ()
handleOpt1 n f s = print (n, f, s)

在上面的代码中,提前解构对象在我看来是一种浪费,因为我可以将数据整齐地捆绑在一起。现在我必须单独传递 Opt1 的每个部分,或者创建一个单独的数据类型来将它们拖走。是否可以将整个Opt1 传递给handleOpt1 而不允许传入一般的Option 实例,例如使handleOpt1 Help 编译错误?

下面的示例伪代码:


data Option
    = Help
    | Opt1 Int Double String

handleOption :: Option -> IO ()
handleOption option = case option of
    Help -> handleHelp
    opt1 @ Opt1{} -> handleOpt1 opt1

handleHelp :: IO ()
handleHelp = print "help"

handleOpt1 :: Option:Opt1 -> IO ()
handleOpt1 (Opt1 n f s) = print (n, f, s)

【问题讨论】:

  • 编译器只能检查类型是否正确。它无法检查传递给handleOpt1 的值是否是使用正确的构造函数构造的(通常;它原则上可以检查源代码中显式传递的值是否正确,但由于通常传递的值是在运行时计算的,所以没有人编写代码来检查异常情况)。
  • 可以为选项创建一个类型类,找出某种方法来使用存在量化来组合它们(并从组合中提取有用的信息)......或者你可以只是不要再为琐碎的事情担心了。如果你的程序中有什么东西影响了你的表现,那可能不是。
  • “浪费”是指设计层面,而不是性能层面。更新了我的帖子。

标签: haskell types constructor


【解决方案1】:

您可以为此使用GADTs

{-# LANGUAGE GADTs #-}

data Option a where
    Help :: Option ()
    Opt1 :: Int -> Double -> String -> Option (Int, Double, String)

handleOption :: Option a -> IO ()
handleOption option = case option of
    Help          -> handleHelp
    opt1 @ Opt1{} -> handleOpt1 opt1

handleHelp :: IO ()
handleHelp = print "help"

handleOpt1 :: Option (Int, Double, String) -> IO ()
handleOpt1 (Opt1 n f s) = print (n, f, s)

使用 GADT,您可以向编译器提供更多类型信息。对于handleOpt1,由于它只接受Option (Int, Double, String),编译器知道Option ()(即Help)永远不会被传入。

也就是说,使用 GADT 会使其他一些事情变得更加困难。例如,自动派生(例如deriving (Eq, Show))通常不适用于它们。您应该仔细考虑在您的案例中使用它们的利弊。

【讨论】:

  • 字体部门看起来有点矫枉过正。
【解决方案2】:

在这个特定的示例中,通过放弃 handleHelphandleOpt1 并让它们成为 handleOption 函数的独立方程来解决“问题”似乎更自然:

handleOption :: Option -> IO ()

handleOption Help = print "help"

handleOption (Opt1 n f s) = print (n, f, s)

这会让你两全其美。您可以为每种情况编写一个单独的方程式(因此,即使每种情况都很大,您也可以防止它们融合成一个巨大的方程式),您不必编写任何样板“调度”函数,也不必命名Opt1 案例的各个部分,直到您真正需要使用它们为止。

【讨论】:

    【解决方案3】:

    GHC 很有可能内联 handleHelphandleOpt1,从而避免调用开销 - look at the generated Core(编译器的中间表示)确定。

    如果由于某种原因这些函数没有被内联,您可以mark them with the INLINE pragma

    handleHelp :: IO ()
    handleHelp = print "help"
    {-# INLINE handleHelp #-}
    
    handleOpt1 :: Option -> IO ()
    handleOpt1 (Opt1 n f s) = print (n, f, s)
    {-# INLINE handleOpt1 #-}
    

    你也可以依靠内联来避免解构handleOption中的参数:

    handleOpt1 :: Option -> IO ()
    handleOpt1 (Opt1 n f s) = print (n, f, s)
    handleOpt1 _ = undefined
    

    undefined 只是为了消除有关非详尽模式匹配的警告。或者,您可以删除此行并为此模块启用-fno-warn-incomplete-patterns

    查看生成的Core我们可以看到handleOpt1undefined分支被淘汰了:

    handleOpt2
      :: Option
         -> State# RealWorld
         -> (# State# RealWorld, () #)
    handleOpt2 =
      \ (ds_dl7 :: Option)
        (eta_Xh :: State# RealWorld) ->
        case ds_dl7 of _ {
          Help -> ...  
          Opt1 n_aaq f_aar s_aas -> ...
    
    main1
      :: State# RealWorld
         -> (# State# RealWorld, () #)
    main1 =
      \ (eta_Xk :: State# RealWorld) ->
        handleOpt2 (Opt1 2 3.0 "") eta_Xk
    

    不过,我更喜欢原始版本,因为它排除了handleOpt1 中模式匹配失败的可能性。

    【讨论】:

    • 我不关心这里的性能。我对类型安全和保持数据捆绑感兴趣。
    【解决方案4】:

    我喜欢 Ben 的回答,但或者,您可以只介绍更多类型。

    data Opt1Params = Opt1Params Int Double String

    数据选项 = 帮助 |选项 1 选项 1 参数

    handleOption 帮助 = handleHelp handleOption (Opt1 参数) = handleOpt1 参数

    handleOpt1 (Opt1Params n f s) = ...

    【讨论】:

      猜你喜欢
      • 2013-08-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多