haskell中的functor

modules

在 Haskell 脚本中导入模块的语法是 import <name of modules>.
这必须在定义任何函数之前完成,因此通常在文件的顶部完成导入.

1
2
3
4
import Data.List
:m + Data.List

ghci> :m + Data.List Data.Map Data.Set

unique import

unique import允许我们仅选择需要的部分导入,而不是整个模块,hiding就是导入补集:

1
2
import Data.List (nub, sort)
import Data.List hiding (nub)

namespace

namespace导入可以避免名称冲突,特别是当多个模块中有相同的函数或变量名时.和python的导入方式差不多

1
2
3
4
5
6
7
import qualified Data.Map 
-- 使用时需要加前缀,如:
Data.Map.filter

import qualified Data.Map as M
-- 之后可以使用缩写形式,如:
M.filter


IO

这里的IO并不能算传统意义上的输入输出,因为IO操作的目的并不是单纯让cpu将数据去和内存或者设备交互,更多的像是一种对传统编程语言的妥协,工程师而非理论学家的要求,规定了很不fp的main函数,以及会涉及有副作用的操作.

main

顾名思义,main和其他语言中的同行差不多,就是系统调用程序文件时的入口

通过 ghc --make test2 编译程序:

1
2
3
4
ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> :t putStrLn "hello, world"
putStrLn "hello, world" :: IO ()

向终端打印字符串没有任何有意义的返回值,因此会使用一个空值 ().

以下是一个简单的 main 示例:

1
2
3
4
main = do
foo <- putStrLn "Hello, what's your name?"
name <- getLine
putStrLn ("Hey " ++ name ++ ", you rock!")

$<-$表示绑定,也可以理解为赋值,而$=$在IO中,或者在整个haskell中,表示起一个别名
do 块中,最后一个动作不能像前两个一样绑定到名字上.

1
name = getLine

以上代码只是为 getLine I/O 动作赋予了一个新名字 name.

副作用

副作用是指一个函数或表达式在计算过程中除了返回值以外还影响了外部状态.例如:

  • 打印输出到屏幕.
  • 修改文件系统(例如创建文件或写入数据).
  • 从终端读取输入.
  • 网络通信.

这些操作都无法用纯函数描述,因为它们的结果不仅依赖输入参数,还依赖外部环境的状态.
eg:

1
2
3
4
main = do
putStrLn "What's your favorite color?"
color <- getLine
putStrLn ("Wow, I like " ++ color ++ " too!")

在这个例子中:

  1. putStrLn 将字符串输出到屏幕,这是一种副作用.
  2. getLine 从用户终端读取输入,这也是一种副作用.

这两个操作会影响外部环境(即终端的状态),因此属于副作用的典型例子.

重要注意事项

I/O 动作只有在以下两种情况下才会执行:

  1. 它们被命名为 main.
  2. 它们位于通过 do 块组合的更大的 I/O 动作中.

使用 do 组合多个 I/O 动作

如下例,注意如果要在IO中使用$=$,使用let是必须的

1
2
3
4
5
6
7
8
main = do
putStrLn "What's your first name?"
firstName <- getLine
putStrLn "What's your last name?"
lastName <- getLine
let bigFirstName = map toUpper firstName
bigLastName = map toUpper lastName
putStrLn $ "Hey " ++ bigFirstName ++ " " ++ bigLastName ++ ", how are you?"



Functor

Functor 是 Haskell 中一个重要的类型类,表示一种可以应用函数来转换其内部值的抽象容器.可以说functor就是函数定义层面的函数,如果用高德纳箭头表示就是

functor的命名是function派生的变体而来,中文翻译的所谓函子实在是有点奇怪,有点像一个霓虹妹的名字,个人认为直呼其名更好辨别

基本定义

Functor 类型类定义了一个核心函数 fmap,用于将一个普通函数应用到一个容器中所有的值上.

1
2
class Functor f where
fmap :: (a -> b) -> f a -> f b
  • f: 是一个类型构造器,比如 Maybe[](列表).
  • fmap: 接受一个函数 (a -> b) 和一个容器 f a,返回一个新的容器 f b.

eg:

1
2
3
4
instance Functor IO where
fmap f action = do
result <- action
return (f result)

这表示先执行 action,然后将结果传递给函数 f.

1
2
3
main = do
line <- fmap (intersperse '-' . reverse . map toUpper) getLine
putStrLn line

上述代码中:

  1. 从终端读取一行输入.
  2. 将输入的字符串转为大写、反转顺序并插入 - 分隔符.
  3. 输出处理后的字符串.

Functor 定律

为了保证 Functor 的一致性,所有实例都需要满足以下两条定律:

  1. 恒等性定律:

    1
    fmap id == id

    映射恒等函数 id 不应该改变容器内的值.

  2. 组合性定律:

    1
    fmap (f . g) == fmap f . fmap g

    先组合函数 fg 再映射,应该等同于分别映射 gf.

eg:

1
2
3
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing

对于 Maybe 类型,fmap 将函数应用到 Just 内的值,而对于 Nothing 则直接返回 Nothing.

Functor 与同态

如果你对同态有基本的了解,就会发现整个functor的规则完全和同态是一回事.在数学中,同态是一种结构保持的映射.

例如,群同态要求映射保持群运算:

1
φ(a * b) = φ(a) * φ(b)

对于 Functor 定律:

  • 恒等性 类似于保持数学结构中的单位元(identity element).
  • 组合性 类似于数学中的映射保持复合运算(composition).

  • 数学中的同态通常作用于更抽象的代数结构(如群、环),而 Functor 是编程中对容器或数据结构的映射.

  • Functor 的目标是通过 fmap 提供一种通用方式操作容器内的值,而不是直接操作容器本身的结构.

换句话说,数学中的同态强调映射的结构保持,而 Functor 更关注函数应用到容器中的值.这个haskell中的性质使自定义的用法更加便捷,大概也是英国数学家的要求吧


Applicative

ApplicativeFunctor 的扩展,增加了在多个容器之间组合函数的能力.

基本定义

1
2
3
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
  • pure: 将一个值放入默认的容器中.
  • <*>: 提取第一个容器中的函数,并将其应用到第二个容器中的值.

eg:

1
2
3
4
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f something

使用例

1
2
3
4
ghci> pure (+) <*> Just 3 <*> Just 5
Just 8
ghci> pure (+) <*> Just 3 <*> Nothing
Nothing

解释:

  1. pure (+) 创建一个包含加法函数的 Maybe 容器.
  2. <*> 将加法函数应用到其他容器的值上.

运算符 <$>

<$>fmap 的别名,用于提高代码的可读性:

1
2
ghci> (++) <$> Just "johntra" <*> Just "volta"
Just "johntravolta"

Newtype

newtype 是 Haskell 中定义新数据类型的一种方式,主要用于优化性能,同时提供类型安全.使用上个用 data 定义差别不大,除了特定的常见或者优化,一般没有必要

特点

  1. 性能优化:与 data 相比,newtype 更加高效,因为它不会引入额外的运行时开销.
  2. 语法限制:newtype 每次只能包含一个字段.

eg:

定义一个新的列表类型 ZipList 并为其实现 Applicative 实例:

1
2
3
4
5
newtype ZipList a = ZipList { getZipList :: [a] }

instance Applicative ZipList where
pure x = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith ($) fs xs)

使用例

1
2
ghci> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100..]
[101,102,103]

上述代码中,ZipList 实现了按位置操作的逻辑,将两个列表中的元素逐一相加.