1

我发现很难在数据类型定义中获得关于编码函数的直觉。这是在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

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

4

3 回答 3

7

真的,函数就像其他任何东西一样只是数据。

Prelude> :i (->)
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

什么?那么,数组上最重要的操作是什么?

Prelude> :t (Data.Array.!)
(Data.Array.!) :: GHC.Arr.Ix i => GHC.Arr.Array ie -> i -> e
Prelude> :t (Data.Vector.! )
(Data.Vector.!) :: Data.Vector.Vector a -> Int -> a

让我们为我们自己的数组类型定义类似的东西:

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

现在我们可以做

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

FnArray> 测试!0
"bla"
FnArray> 测试!1
"foo"
FnArray> 测试!2
"*** 例外::8:5-34: 功能测试中的非详尽模式

将此与

Prelude Data.Vector> let test = fromList ["bla", "foo"]
Prelude Data.Vector> test ! 0
"bla"
Prelude Data.Vector> 测试!1
"foo"
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 :<-) 它是一个函子,但属于另一种类型:逆变函子

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 -> Tree类似于Array Tree,除了我们不使用Ints 进行索引而是使用Bools。并且只有两个可评估的布尔值。固定大小为 2 的数组通常称为元组。Bool->Tree ≅ (Tree, Tree)我们有Node Int (Bool->Tree) ≅ Node Int Tree Tree.

诚然,这并不是那么有趣。对于来自固定域的函数,同构通常很明显。有趣的情况是函数域和/或共域上的多态,这总是会导致一些抽象的结果,例如状态单子。但即使在这些情况下,您也可以记住,在 Haskell 中没有什么能真正将函数与其他数据类型分开。

于 2013-08-19T21:42:39.437 回答
1

让我们看看这是否有帮助。不幸的是,对于初学者来说,State 的定义在左右两边都引用了 State,但是它们的含义不同:一个是类型的名称,另一个是构造函数的名称。所以定义真的是:

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

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

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

        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
于 2013-08-20T11:26:29.157 回答
1

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

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

    示例:add :: Int -> Int -> Int与 相同add :: Int -> (Int -> Int)。add 是(函数)类型,它将整数映射到将整数映射到整数的(函数)类型。

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

考虑到以上几点,您会发现数据类型和函数之间的界限不再存在。

至于选择哪种类型,它完全取决于您要解决的问题域。基本上,当您发现需要某种从一种类型到另一种类型的映射(->)时,您将使用该类型。

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

于 2013-08-20T09:00:38.417 回答