仿函数如何在哈斯克尔工作?

时间:2012-10-30 08:00:14

标签: haskell functional-programming functor

我正在努力学习Haskell,而且我正在学习所有基础知识。但是现在我被卡住了,试图让我的头围绕着仿函数。

我读过“仿函数将一个类别转换为另一个类别”。这是什么意思?

我知道要问的很多,但是有人能给我一个简单的英语解释函子或者简单的用例吗?

5 个答案:

答案 0 :(得分:113)

我意外地写了

Haskell Functors教程

我将使用示例回答您的问题,并且我会在评论中将这些类型放在下面。

注意类型中的模式。

fmapmap

的概括

Functors用于为您提供fmap功能。 fmap的作用类似于map,因此请先查看map

map (subtract 1) [2,4,8,16] = [1,3,7,15]
--    Int->Int     [Int]         [Int]

因此它使用 列表中的函数(subtract 1) 。实际上,对于列表,fmap确实可以解释map所做的事情。让我们这次将所有东西乘以10:

fmap (* 10)  [2,4,8,16] = [20,40,80,160]
--  Int->Int    [Int]         [Int]

我将此描述为映射列表中乘以10的函数。

fmap也适用于Maybe

我可以fmap还能做什么?我们使用Maybe数据类型,它有两种类型的值NothingJust x。 (Nothing表示答案,您可以使用Just x表示无法获得答案。)

fmap  (+7)    (Just 10)  = Just 17
fmap  (+7)     Nothing   = Nothing
--  Int->Int  Maybe Int    Maybe Int

好的,fmap再次使用(+7) 里面的 Maybe。 我们也可以fmap其他功能。 length找到列表的长度,因此我们可以通过Maybe [Double]

对其进行fmap
fmap    length             Nothing                      = Nothing
fmap    length    (Just [5.0, 4.0, 3.0, 2.0, 1.573458]) = Just 5
--  [Double]->Int         Maybe [Double]                  Maybe Int

实际上length :: [a] -> Int但是我在[Double]上使用它,所以我专注于它。

让我们使用show将内容转化为字符串。秘密地show的实际类型是Show a => a -> String,但这有点长,我在Int上使用它,所以它专门用于{{1} }}

Int -> String

另外,回顾列表

fmap  show     (Just 12)  = Just "12"
fmap  show      Nothing   = Nothing
-- Int->String  Maybe Int   Maybe String

fmap show [3,4,5] = ["3", "4", "5"] -- Int->String [Int] [String] 适用于fmap

让我们在稍微不同的结构Either something上使用它。类型Either的值为Either a b值或Left a值。有时我们使用Either来表示成功Right b或失败Right goodvalue,有时只是将两种类型的值混合在一起。无论如何,Either数据类型的仿函数仅适用于Left errordetails - 它只留下Right个值。这是有道理的,特别是如果你使用正确的价值作为成功价值(实际上我们不能能够使其适用于两者,因为类型不是't&t; t必然相同)。让我们使用类型Left作为示例

Either String Int

它使fmap (5*) (Left "hi") = Left "hi" fmap (5*) (Right 4) = Right 20 -- Int->Int Either String Int Either String Int 在Either中工作,但对于Eithers,只有(5*)值会发生变化。但是我们可以在Right上进行相反的操作,只要该函数适用于字符串。让我们使用Either Int String", cool!"放在内容的末尾。

(++ ", cool!")

在IO

上使用fmap (++ ", cool!") (Left 4) = Left 4 fmap (++ ", cool!") (Right "fmap edits values") = Right "fmap edits values, cool!" -- String->String Either Int String Either Int String 特别酷

现在,我最喜欢使用fmap的一种方法是在fmap值上使用它来编辑某些IO操作给我的值。让我们举个例子,让你输入内容,然后立即打印出来:

IO

我们可以用一种让我感觉更整洁的方式写出来:

echo1 :: IO ()
echo1 = do
    putStrLn "Say something!"
    whattheysaid <- getLine  -- getLine :: IO String
    putStrLn whattheysaid    -- putStrLn :: String -> IO ()

echo2 :: IO () echo2 = putStrLn "Say something" >> getLine >>= putStrLn 做了一件又一件事,但我喜欢这个的原因是因为>>接受>>=给我们的字符串并将其提供给getLine串。 如果我们只想问候用户该怎么办:

putStrLn

如果我们想以更整洁的方式写出来,我有点卡住了。我必须写

greet1 :: IO ()
greet1 = do
    putStrLn "What's your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name)

greet2 :: IO () greet2 = putStrLn "What's your name?" >> getLine >>= (\name -> putStrLn ("Hello, " ++ name)) 版本更好。事实上,do符号是存在的,所以你不必这样做。但do可以救援吗?是的,它可以。 fmap是一个我可以在getLine上进行fmap的函数!

("Hello, "++)

我们可以像这样使用它:

fmap ("Hello, " ++)  getLine   = -- read a line, return "Hello, " in front of it
--   String->String  IO String    IO String

我们可以根据我们提供的任何事情来解决这个问题。让我们不同意&#34; True&#34;或&#34;错误&#34;输入:

greet3 :: IO ()
greet3 = putStrLn "What's your name?" 
         >> fmap ("Hello, "++) getLine >>= putStrLn

或者让我们只报告文件的大小:

fmap   not      readLn   = -- read a line that has a Bool on it, change it
--  Bool->Bool  IO Bool       IO Bool

结论:fmap length (readFile "test.txt") = -- read the file, return its length -- String->Int IO String IO Int -- [a]->Int IO [Char] IO Int (more precisely) 做了什么,它做了什么?

如果您一直在观察类型中的模式并思考这些示例,那么您会注意到fmap采用的函数可以处理某些值,并将该函数应用于具有或生成这些函数的函数。值以某种方式编辑值。 (例如readLn是为了阅读Bool,所以类型fmap中有一个布尔值,因为它产生IO Bool,eg2 Bool[4,5,6]在其中。)

Int

这适用于列举的东西(书面fmap :: (a -> b) -> Something a -> Something b ),[]MaybeEither StringEither Int以及大量内容。如果它以合理的方式工作,我们将其称为Functor(有一些规则 - 稍后)。实际的fmap类型是

IO

但为了简洁起见,我们通常会将fmap :: Functor something => (a -> b) -> something a -> something b 替换为something。但是,编译器完全相同:

f

回顾一下这些类型,然后检查一下这种情况总是有效 - 关于fmap :: Functor f => (a -> b) -> f a -> f b 的事情 - 那时候Either String Int是什么时候?

附录:Functor规则是什么,我们为什么要这些规则?

f是身份功能:

id

以下是规则:

id :: a -> a
id x = x

首先是身份标识:如果你映射不起作用的函数,那就不会改变任何东西。这听起来很明显(很多规则都有),但是你可以解释为fmap id == id -- identity identity fmap (f . g) == fmap f . fmap g -- composition 允许更改值,而不是结构。不允许fmapfmap变为Just 4,或Nothing变为[6],或[1,2,3,6]变为Right 4因为不仅仅是数据发生了变化 - 这些数据的结构或上下文发生了变化。

我在处理图形用户界面项目时曾经遇到过这个规则 - 我希望能够编辑这些值,但是如果不改变下面的结构,我就无法做到这一点。没有人会真正注意到这种差异,因为它具有相同的效果,但是意识到它没有遵守仿函数规则让我重新思考我的整个设计,现在它更清洁,更光滑,更快。 / p>

其次是构图:这意味着您可以选择是一次fmap一个函数,还是同时对它们进行fmap。如果Left 4单独留下您的值的结构/上下文,并且只使用给定的函数编辑它们,那么它也适用于此规则。

为什么我们有它们?确保fmap不会在幕后悄悄地做任何事情或改变我们没有预料到的任何事情。它们不是由编译器强制执行的(要求编译器在编译代码之前证明一个定理是不公平的,并且会减慢编译速度 - 程序员应该检查)。这意味着你可以作弊,但这是一个糟糕的计划,因为你的代码会产生意想不到的结果。

答案 1 :(得分:55)

模糊解释是Functor是某种容器和关联函数fmap,它允许你改变包含的内容,给定一个转换包含的函数。

例如,列表就是这种容器,fmap (+1) [1,2,3,4]会产生[2,3,4,5]

Maybe也可以成为仿函数,以便fmap toUpper (Just 'a')产生Just 'A'

fmap的一般类型非常清楚地显示了正在发生的事情:

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

专业版可能会更清晰。这是列表版本:

fmap :: (a -> b) -> [a] -> [b]

和Maybe版本:

fmap :: (a -> b) -> Maybe a -> Maybe b

您可以通过使用Functor查询GHCI来获取有关标准:i Functor实例的信息,并且许多模块定义了Functor s(以及其他类型类)的更多实例。

但请不要太认真地对待“容器”这个词。 Functor是一个定义明确的概念,但你可以用这种模糊的类比来推理它。

理解正在发生的事情的最好方法是简单地阅读每个实例的定义,这应该让您直观了解正在发生的事情。从那里开始,真正形式化您对概念的理解只是一小步。需要补充的是澄清我们的“容器”究竟是什么,并且每个实例都满足于一对简单的法则。

答案 2 :(得分:12)

将仿函数本身与应用了仿函数的类型中的值区分开来是非常重要的。仿函数本身是类型构造函数,如MaybeIO或列表构造函数[]。仿函数中的值是应用了该类型构造函数的类型中的某个特定值。例如Just 3Maybe Int类型中的一个特定值(该类型是应用于Maybe类型的Int仿函数),putStrLn "Hello World"是该类型中的一个特定值键入IO ()[2, 4, 8, 16, 32][Int]类型中的一个特定值。

我喜欢考虑一个类型的值,其中一个仿函数被应用为&#34;相同的&#34;作为基类型中的值,但带有一些额外的&#34; context&#34;。人们经常使用容器类比作为仿函数,这对于相当多的仿函数非常自然地起作用,但是当你不得不说服自己IO或{{1}时,它会变得更加困扰而不是帮助就像一个容器。

因此,如果(->) r表示整数值,则Int表示可能不存在的整数值(&#34;可能不存在&#34;是&#34;上下文&#34)。 Maybe Int表示具有多个可能值的整数值(这与列表仿函数的解释与&#34; nondeterminism&#34;对列表monad的解释相同)。 [Int]表示整数值,其精确值取决于整个Universe(或者,它表示可以通过运行外部进程获得的整数值)。 IO Int是任意Char -> Int值的整数值(&#34;将Char作为参数的函数&#34;是任何类型r的仿函数; { {1}} r r是类型构造函数,它是一个仿函数,应用于Char,在中缀表示法中变为(->) CharInt。< / p>

使用常规仿函数,唯一可以做的就是(->) Char Int,其类型为Char -> Intfmap将一个对正常值进行操作的函数转换为一个函数,该函数对值进行操作,并使用仿函数添加其他上下文;这对于每个仿函数到底有什么不同,但你可以用它们来完成。

因此Functor f => (a -> b) -> (f a -> f b)仿函数fmap是一个函数,它计算一个可能不存在的整数1,它高于其输入可能不存在的整数。使用列表仿函数Maybe是计算高于其输入非确定性整数的非确定性整数1的函数。使用fmap (+1)仿函数,fmap (+1)是计算比其输入整数高1的整数的函数 - 其值取决于外部宇宙。使用IO仿函数,fmap (+1)是一个函数,它将1加到一个取决于(->) Char的整数(当我将fmap (+1)提供给返回值时,我得到1通过将相同的Char提供给原始值,比我得到的更高。

但总的来说,对于某些未知的仿函数CharChar应用于f中的某些值是&#34;仿函数版本&#34;普通fmap (+1)上的函数f Int。它在任何类型的&#34; context&#34;中将整数加1。这个特殊的函子有。

就其本身而言,(+1)并不一定有用。通常,当您编写具体程序并使用仿函数时,您正在使用一个特定仿函数,并且您经常将Int视为该特定仿函数的 < / em>的。当我使用fmap时,我经常不会将我的fmap值视为非确定性整数,我只是将它们视为整数列表,我想到{{ 1}}与我对[Int]的看法相同。

那么为什么要去玩弄呢?为什么不只有[Int]列表,fmap用于mapmap用于applyToMaybe?然后每个人都会知道他们做了什么,没有人必须理解奇怪的抽象概念,如仿函数。

关键是认识到那里有一个很多的仿函数;几乎所有的容器类型都是一个开始(因此容器类比为仿函数 )。即使我们没有仿函数,他们每个人都有一个与Maybe相对应的操作。无论何时只根据applyToIO操作(或IO,或者为您的特定类型调用的任何内容)编写算法,那么如果您根据仿函数而不是您的函数来编写算法特定类型,然后它适用于所有仿函数。

它也可以作为一种文件形式。如果我将我的一个列表值移交给您编写的在列表上运行的函数,它可以执行任何操作。但是,如果我将我的列表移交给你编写的函数,该函数在任意仿函数中对值进行操作,那么我知道你的函数的实现不能使用列表特征,只有函子特征

回想一下如何在传统的命令式编程中使用functorish的东西可能有助于看到它的好处。像数组,列表,树等容器类型通常会有一些用于迭代它们的模式。虽然库通常提供标准的迭代接口来解决这个问题,但它对于不同的容器可能略有不同。但是每次你想迭代它们时你仍然会写一个for循环,当你想要做的是计算容器中每个项目的结果并收集你通常最终混合在逻辑中的所有结果用于构建新容器。

fmap每个 for循环的那个表单,你将要编写,由图书馆编写者一次性地排序,然后你甚至坐下来编程。此外,它还可以用于fmapmap之类的内容,这些内容可能与在命令式语言中设计一致的容器界面无关。

答案 3 :(得分:7)

在Haskell中,仿函数捕获了容器“东西”的概念,这样你就可以在不改变容器形状的情况下操纵那些“东西”。

Functors提供了一个函数fmap,它允许你通过使用常规函数并将其从一种元素的容器“提升”到另一个元素到另一个元素来执行此操作:

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

例如,列表类型构造函数[]是一个仿函数:

> fmap show [1, 2, 3]
["1","2","3"]

等许多其他Haskell类型构造函数,如MaybeMap Integer 1

> fmap (+1) (Just 3)
Just 4
> fmap length (Data.Map.fromList [(1, "hi"), (2, "there")])
fromList [(1,2),(2,5)]

请注意,fmap不允许更改容器的“形状”,因此,例如,如果您fmap列表,则结果具有相同数量的元素,如果您fmap 1}} Just它不能成为Nothing。在形式上,我们要求fmap id = id,即如果你fmap身份函数,则没有任何变化。

到目前为止,我一直在使用术语“容器”,但它确实比这更普遍。例如,IO也是一个仿函数,在这种情况下,“形状”的含义是fmap动作上的IO不应改变副作用。事实上,任何monad都是一个算子 2

在类别理论中,仿函数允许您在不同类别之间进行转换,但在Haskell中我们只有一个类别,通常称为Hask。因此,Haskell中的所有仿函数都从Hask转换为Hask,因此它们就是我们所说的endofunctors(从类别到其自身的仿函数)。

最简单的形式,仿函数有点无聊。只需一次操作就可以做到这一点。但是,一旦你开始添加操作,你可以从常规的仿函数到应用仿函数到monad,事情会很快变得更有趣,但这超出了这个答案的范围。

1 Set不是,因为它只能存储Ord种类型。函数必须能够包含任何类型。
2 由于历史原因,Functor不是Monad的超类,尽管很多人认为它应该是。

答案 4 :(得分:3)

让我们来看看类型。

Prelude> :i Functor
class Functor f where fmap :: (a -> b) -> f a -> f b

但这意味着什么?

首先,f是一个类型变量,它代表一个类型构造函数:f a是一个类型; a是一个代表某种类型的类型变量。

其次,给定一个函数g :: a -> b,您将获得fmap g :: f a -> f b。即fmap g是一个函数,将f a类型的内容转换为f b类型的内容。请注意,我们无法在此处查看a类型和b类型的内容。函数g :: a -> b以某种方式处理f a类型的事物,并将它们转换为f b类型的事物。

请注意f是相同的。只有其他类型更改。

这是什么意思?它可能意味着很多事情。 f通常被视为东西的“容器”。然后,fmap g使g能够对这些容器的内部进行操作,而不会打开它们。结果仍然包含在“内部”中,类型类Functor不能为我们提供打开它们或查看内部的能力。只是在不透明的东西中进行一些转变就是我们得到的。任何其他功能都必须来自其他地方。

另请注意,并没有说这些“容器”只带有a类型的一个“东西”;它内部可以有许多单独的“事物”,但所有类型a都是相同的。

最后,任何仿函数的候选人都必须服从the functor laws

fmap id === id
fmap (f . g) === fmap f . fmap g