我发现很难在数据类型定义中获得关于编码函数的直觉。这在State
和IO
类型的定义中完成,例如
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
我不确定我在这里要做什么,但是什么可以是我可以在这种类型中编码的函数的示例?为什么我必须在类型中对其进行编码,但不能将其用作普通函数,我不知道,可能在需要时作为参数传递?
答案 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 -> Tree
与Array 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 -> Int
与add :: Int -> (Int -> Int)
相同。 add是(function)类型,它将Integer映射到将Integer映射到Integer的(function)类型。
(->)
类型构造函数。根据以上几点思考,您会发现数据类型和功能之间的界限不再存在。
就选择哪种类型而言,它仅取决于您尝试解决的问题域。基本上,当您需要从一种类型到另一种类型需要某种映射时,您将使用(->)
类型。
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