真的,函数就像其他任何东西一样只是数据。
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
,除了我们不使用Int
s 进行索引而是使用Bool
s。并且只有两个可评估的布尔值。固定大小为 2 的数组通常称为元组。Bool->Tree ≅ (Tree, Tree)
我们有Node Int (Bool->Tree) ≅ Node Int Tree Tree
.
诚然,这并不是那么有趣。对于来自固定域的函数,同构通常很明显。有趣的情况是函数域和/或共域上的多态,这总是会导致一些抽象的结果,例如状态单子。但即使在这些情况下,您也可以记住,在 Haskell 中没有什么能真正将函数与其他数据类型分开。