为什么在数据类型定义中编码函数?

时间:2013-08-19 21:22:53

标签: function haskell types

我发现很难在数据类型定义中获得关于编码函数的直觉。这在StateIO类型的定义中完成,例如

data State s a = State s -> (a,s)
type IO a = RealWorld -> (a, RealWorld) -- this is type synonym though, not new type

我想看一个更简单的例子来理解它的价值,所以我可以在此基础上建立更复杂的例子。对于例如说我有一个数据结构,在一个数据构造函数中编码函数是否有意义。

data Tree = Node Int (Tree) (Tree) (? -> ?) | E

我不确定我在这里要做什么,但是什么可以是我可以在这种类型中编码的函数的示例?为什么我必须在类型中对其进行编码,但不能将其用作普通函数,我不知道,可能在需要时作为参数传递?

3 个答案:

答案 0 :(得分:7)

实际上,函数就像其他任何数据一样。

  

前奏> :我( - >)
  data (->) a b -- Defined in`GHC.Prim'
  instance Monad ((->) r) -- Defined in`GHC.Base'
  instance Functor ((->) r) -- Defined in`GHC.Base'

如果您只考虑来自Int的函数,那么这很自然地出现并且没有任何概念上令人惊讶的事情。我会给他们一个奇怪的名字:(记住(->) a b表示a->b

type Array = (->) Int

什么的?那么,阵列上最重要的操作是什么?

  

前奏> :t(Data.Array。!)
  (Data.Array。!):: GHC.Arr.Ix i => GHC.Arr.Array i e - >我 - > Ë
  前奏> :t(Data.Vector。!)
  (Data.Vector。!):: Data.Vector.Vector a - > Int - >一个

让我们为自己的数组类型定义类似的内容:

(!) :: Array a -> Int -> a
(!) = ($)

现在我们可以做到

test :: Array String
test 0 = "bla"
test 1 = "foo"
  

FnArray>测试! 0
  “喇嘛”
  FnArray>测试! 1
  “富”
  FnArray>测试! 2
  “***异常:: 8:5-34:功能测试中的非详尽模式

将此与

进行比较
  

Prelude Data.Vector>让test = fromList [“bla”,“foo”]
  Prelude Data.Vector>测试! 0
  “喇嘛”
  Prelude Data.Vector>测试! 1
  “富”
  Prelude Data.Vector>测试! 2
  “***例外:./ Data / Vector / Generic.hs:244((!)):索引超出范围(2,2)

没有那么不同,对吗?这是Haskell对参照透明度的强制执行,它保证了函数的返回值实际上可以解释为某个容器的居民值。这是查看Functor实例的一种常用方法:fmap transform f将一些转换应用于f中的“包含”值(作为结果值)。这可以通过简单地在目标函数之后组成转换来实现:

instance Functor (r ->) where
  fmap transform f x = transform $ f x

(虽然你当然更好地写这个fmap = (.)。)


现在,更令人困惑的是(->)类型构造函数还有一个类型参数:参数类型。让我们通过定义

来关注它
{-# LANGUAGE TypeOperators #-}

newtype (:<-) a b = BackFunc (b->a)

要感受一下:

show' :: Show a  =>  String :<- a
show' = BackFunc show

即。它实际上只是反过来写的功能箭头。

(:<-) Int是某种容器,与(->) Int类似于数组的方式类似吗?不完全的。我们无法定义instance Functor (a :<-)。然而,从数学角度讲,(a :<-) 一个仿函数,但不同类型:contravariant functor

instance Contravariant (a :<-) where
  contramap transform (BackFunc f) = BackFunc $ f . transform

“普通”仿函数OTOH是协变仿函数。如果直接比较,命名很容易理解:

fmap      :: Functor f       => (a->b) -> f a->f b
contramap :: Contravariant f => (b->a) -> f a->f b

虽然逆变函数并不像协变函数那样常用,但在推理数据流等时可以大致相同的方式使用它们。当在数据字段中使用函数时,它应该是协变的,而不是逆变的。想一想,不是函数与价值观 - 因为实际上,与纯函数式语言中的“静态值”相比,函数没有什么特别之处。


关于您的Tree类型

我认为这种数据类型不能成为真正有用的东西,但我们可以用类似的类型做一些愚蠢的事情,可以说明我在上面提出的观点:

data Tree' = Node Int (Bool -> Tree) | E

也就是说,对性能的反思,与通常的同构

data Tree = Node Int Tree Tree | E

为什么呢?好吧,Bool -> TreeArray Tree类似,但我们不使用Int来编制索引,而是使用Bool。并且只有两个可评估的布尔值。固定大小为2的数组通常称为元组。使用Bool->Tree ≅ (Tree, Tree)我们有Node Int (Bool->Tree) ≅ Node Int Tree Tree

不可否认,这并不是那么有趣。对于来自固定域的函数,同构通常是显而易见的。有趣的案例在函数域和/或codomain上是多态的,这总是会导致一些抽象的结果,例如状态monad。但即使在这些情况下,你也记得在Haskell中没有任何东西能真正分离其他数据类型的函数。

答案 1 :(得分:1)

您通常使用2个概念启动FP学习 - 数据类型函数。一旦你对使用这两个概念设计程序有很好的信心,我建议你开始只使用一个概念,即类型,这意味着:

  • 您可以通过组合语言中的现有类型或类型构造函数来定义新类型。
  • 您可以定义新类型构造函数,以在问题域中抽象出一般概念。
  • 函数只是一种特定类型映射到另一种类型的类型。这基本上意味着函数映射的类型本身可以是函数等等(因为我们只是说函数是类型)。这就是人们通常所说的更高的函数,这也让你觉得函数需要多个参数,而现实是函数类型总是将一个类型映射到另一个类型(即它是一元函数),但我们知道另一种类型本身可以是一种函数类型。

    示例:add :: Int -> Int -> Intadd :: Int -> (Int -> Int)相同。 add是(function)类型,它将Integer映射到将Integer映射到Integer的(function)类型。

  • 要创建Function类型,我们使用Haskell提供的(->)类型构造函数。

根据以上几点思考,您会发现数据类型和功能之间的界限不再存在。

就选择哪种类型而言,它仅取决于您尝试解决的问题域。基本上,当您需要从一种类型到另一种类型需要某种映射时,您将使用(->)类型。

State是使用函数类型定义的,因为我们在FP中表示状态的方式是“采用当前状态并返回值和新状态的映射”,因为您可以看到存在映射发生在这里,因此使用(->)类型。

答案 2 :(得分:1)

让我们看看这是否有帮助。不幸的是,对于初学者来说,状态引用的定义既包括左侧和右侧的状态,但它们具有不同的含义:一个是类型的名称,另一个是构造函数的名称。所以定义真的是:

        data State s a = St (s -> (a,s))

这意味着您可以使用构造函数St构造State sa类型的值,并将函数从s传递给(a,s),也就是说,可以构造某个类型a的值和下一个值的函数来自前一州的州。这是表示状态转换的简单方法。

为了了解为什么传递函数很有用,你需要研究其余部分是如何工作的。例如,我们可以通过组合函数来构造State s类型的新值。通过组合这样的状态,这样的状态转换函数,你得到一个状态机,然后可以用它来计算一个初始状态的值和最终状态。

        runStateMachine :: State s a -> s -> (a,s)
        runStateMachine (St f) x = f x   -- or shorter, runStateMachine (St f) = f -- just unwrap the function from the constructor