和类型的自定义文本表示

时间:2015-12-11 19:12:44

标签: haskell

我无法解决如何在不进行大量模式匹配的情况下将和类型转换为字符串的问题。 [简化]示例可能是这样的:

data Shape = Rectangle Int Float Float |
             Circle Int Float |
             Ellipse Int Float Float
             deriving (Show, Eq) 

我想要的是一个函数renderShape :: Shape -> String,它接受​​Shape并给我一个以某种方式表示参数的字符串(我在模拟程序的输入文件中生成行) 。请注意,我希望这与show不同,因为除了将数据结构转换为字符串之外,它还会进行一些额外的格式化。我实际需要它做的有点复杂,但是出于示例目的(因为如果你能解释一般如何做到这一点,我想我可以解决剩下的问题),让我们说我想在结尾处加上一个分号,用逗号分隔,例如

renderShape $ Rectangle 1 2 2
>> "Rectangle,1,2,2;"

具体来说,我正在努力的是该类型的不同构造函数具有不同数量的参数。我可以用模式匹配做点什么,比如

renderShape (Rectangle i x y) = (intercalate "," ["Rectangle", show i, show x, show y]) ++ ";"
renderShape (Circle i x) = ...
...

但是我不想在现实中我有很多这些类型的形状,所以它非常繁琐。

我通过滥用派生的show实例并简单地转换字符串找到了“有用”的东西,但它看起来真的很难看:

{-# LANGUAGE OverloadedStrings #-}
import Data.List (intercalate)

renderShape :: Shape -> String
renderShape s = intercalate "," theWords ++ ";"
  where theWords = (words . show) s

所以我的问题是,做到这一点的“好”方法是什么?在我看来,必须有一个简单而干净的方式来解决它,但我无法弄清楚我的生活。我对Haskell也没有太大的经验,所以如果我的整个方法从根本上是错误的或非惯用的,那么我会欢迎替代方案。

2 个答案:

答案 0 :(得分:3)

这是泛型的理想用法。我们的策略是将数据类型转换为其通用表示形式(from :: (Generic a) => a -> Rep a),然后递归到Rep aRep a实际上是一个类型函数,所以让我们看看它是什么样的:

λ> :info Shape
[lots of garbage]
type instance Rep Shape
  = D1
      Main.D1Shape
      (C1
         Main.C1_0Shape
         (S1 NoSelector (Rec0 Int)
          :*: (S1 NoSelector (Rec0 Float) :*: S1 NoSelector (Rec0 Float)))
       :+: (C1
              Main.C1_1Shape
              (S1 NoSelector (Rec0 Int) :*: S1 NoSelector (Rec0 Float))
            :+: C1
                  Main.C1_2Shape
                  (S1 NoSelector (Rec0 Int)
                   :*: (S1 NoSelector (Rec0 Float) :*: S1 NoSelector (Rec0 Float)))))

OOF。我们在这里处于较高的领土。要遍历此数据结构,我们需要递归到类型本身。虽然值递归通过在数据结构的较小和较小部分上重复调用函数来工作,但我们将创建一个名为Jason的单方法类,然后在该类型的较小和较小部分上实例化该方法。

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}

import GHC.Generics

data Shape = Rectangle Int Float Float |
             Circle Int Float |
             Ellipse Int Float Float
             deriving (Show, Eq, Generic)

class Jason a where
  jasonShow :: a -> String

-- Integers can use the regular show.
instance Jason Float where jasonShow = show
-- So can floats.
instance Jason Int where jasonShow = show

-- Constant values are easy.
instance (Jason c) => Jason (K1 i c p) where jasonShow = jasonShow . unK1

-- Use generics to pattern match into constructors.
instance (Jason (f p), Constructor c) => Jason (M1 C c f p) where
  jasonShow constructor@(M1 x) = conName constructor ++ "," ++ jasonShow x

-- We don't care about selectors.
instance (Jason (f p)) => Jason (M1 S c f p) where
  jasonShow (M1 x) = jasonShow x

-- Or whether something is a datatype or not.
instance (Jason (f p)) => Jason (M1 D c f p) where
  jasonShow (M1 x) = jasonShow x

-- We don't care about the index into the disjoint union (a.k.a. "|") of a datatype.
instance (Jason (f p), Jason (g p)) => Jason ((f :+: g) p) where
  jasonShow (L1 x) = jasonShow x
  jasonShow (R1 x) = jasonShow x

-- We want to insert a comma when we encounter a product in the datatype.
instance (Jason (f p), Jason (g p)) => Jason ((f :*: g) p) where
  jasonShow (x :*: y) = jasonShow x ++ "," ++ jasonShow y

renderShape :: Shape -> String
renderShape = (++ ";") . jasonShow . from

要输出产品的逗号和构造函数的名称,我们特别设置了这些实例:

  • M1 C c f p是包含值M的构造函数(C)的元信息(f p)的类型。对此类型的值调用conName将有助于返回表示构造函数名称的字符串。

  • (f :*: g) p是两种类型f pg p值的乘积的类型。早些时候,当我们查询矩形的类型时,您可以看到它出现在几个地方。例如M1 S NoSelector (Rec0 Float) :*: M1 S NoSelector (Rec0 Float)(我扩展了type S1 = M1 S别名)。您可以将:*:视为Rectangle 1 2 2中数字之间的粘合剂。

  • 递归中的基本案例是IntFloat。我们可以继续定义更多基础案例,但我们的形状只是整数和浮动。

这是非常奇怪的代码!你没有看到如此多的神秘数据类型,每天都有奇怪的双字符名称。但我们确实得到了这个好结果:

λ> renderShape (Rectangle 1 2 2)
"Rectangle,1,2.0,2.0;"
λ> renderShape (Circle 1 2)
"Circle,1,2.0;"
λ> renderShape (Ellipse 1 2 2)
"Ellipse,1,2.0,2.0;"

这也是很多代码。但它是泛型代码,这意味着您可以将其与其他非Shape数据类型一起重用。

语言扩展

  • DeriveGeneric:允许derive (Generic),我们的数据类型Generic自动生成Shape

  • FlexibleContexts:没有这个,我们不能说instance (Jason (f p), Constructor c) => Jason (M1 C c f p)。 Haskell规范不允许像Jason (f p)那样的约束。 Jason p没问题,但Jason (f p)不行。幸运的是,GHC很灵活。

  • FlexibleInstances:允许将M1 i c f p破坏为三个不同的实例:M1 C c f pM1 D c f pM1 S c f p。通常不允许使用规范。

  • TypeOperators:允许使用中缀类型函数:*::+:

答案 1 :(得分:2)

您可以为您的ADT派生Data.Data,它为任何自定义数据类型公开通用描述符。然后你可以编写一个函数来呈现Data的任何实例,使其成为你需要的非常类似的Show方式。对于像Shape这样的小型类型来说肯定有点过头了,但你可以将它重用于多种更大的类型。

我并不完全熟悉这样做的API,但是http://chrisdone.com/posts/data-typeable以编写一个签名接近gshow :: Data d => d -> String的通用函数的示例结束。

相关问题