写给程序员看的函数式对话 7 - Maybe 和模式匹配

1,275 阅读4分钟

学生:好久不见啊,今天又有时间来聊天啊

方:嗯,今天想跟你聊聊 Maybe 和模式匹配

直接上 TypeScript 代码:

const addMark = (whatever?: string) => whatever + '!'
addMark('Frank') 
// 输出 Frank!
addMark() 
// 输出 undefined!

最后输出的 undefined! 并不是我们想要的输出,一般你会怎么解决这样的问题?

学生:「判空」呗

方:没错,代码差不多是这样:

const addMark = (whatever?: string) => {
  if(whatever !== undefined){
    return whatever + '!'
  } else {
    return '!'
  }
}
 
addMark() 
// 输出 !

我问问你,现在 whatever 的类型是什么?

学生:string 呀

方:undefined 也是 string 吗?

学生:哦,我懂你意思了,whatever 的类型是 string | undefined

方:现在我给你介绍另一种思路,我们可以用 Maybe<string> 表示 whatever 的类型

学生:听不懂,代码怎么写

方:代码:

type Just<X> = { _type: 'Just', value: X }
type Nothing = { _type: 'Nothing' }
type Maybe<X> = Nothing | Just<X>
const createMaybe = 
  <T>(value:T): Maybe<T> => 
    value === undefined ? {_type: 'Nothing'} : {_type: 'Just', value}


const addMark = (whatever: Maybe<string>) => {
  if(whatever._type === 'Just' ){
    return whatever.value + '!'
  } else if(whatever.type === 'Nothing') {
    return '!'
  }
}
const readStringFromFile = ()=>{
  return createMaybe<string>('hi')
}

const fileContent = readStringFromFile()

console.log(addMark(fileContent))

学生:确实是没有 undefined 和 null 了,但是你还是要判断 whatever._type 是 'Just' 还是 'Nothing' 不是吗?

方:是的,这是 JS 的表达能力有限所致,如果用 Haskell 写,配合模式匹配,代码就相当简洁了:

-- [Char] 就是 String
readStringFromFile :: [Char] -> Maybe [Char] 
readStringFromFile path = Just "hi" 
-- 文件可能不存在,返回空,这里我写死返回 "hi"


addMark :: Maybe [Char] -> [Char]
addMark (Just str) = str ++ "!"
addMark Nothing = "!"


main :: IO ()
main = do 
  print $ addMark $ readStringFromFile "./1.txt"

-- 输出 "hi!"

你看,没有 null / undefined,也没有 if else。

学生:模式匹配是什么?

方:其实很简单,我们只看 addMark

addMark :: Maybe [Char] -> [Char]
-- addMark 的参数类型是 Maybe [Char]
-- Maybe [Char] 只有两种情况:Just [Char] 和 Nothing

-- 如果是 Just [Char] 就给 str 后面加上感叹号
addMark (Just str) = str ++ "!"
-- 如果是 Nothing 就直接返回感叹号
addMark Nothing = "!"

学生:看起来跟 switch ... case 差不多啊

方:不一样,switch ... case 是对具体的「值」做比较,模式匹配则是一种「形式上」的匹配,更抽象一些。

接下来我们来练习一下模式匹配,这是斐波那契:

fib :: Integer -> Integer
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2) 
-- 这个版本极慢,有待优化

这是快排:

qs :: [Int] -> [Int]
qs [] = []
qs (first:rest) =
  qs (filter (<= first) rest) 
    ++ [first] 
    ++ qs (filter (> first) rest)

你可以明显地看到,有了模式匹配,几乎就不用再写 if else 了。

学生:还挺方便,那 JS 为什么不引入模式匹配呢?

方:JS 也想引入,不过还在讨论阶段,这里有一个提案,具体代码长这样:

const res = await fetch(jsonService)
case (res) {
  when {status: 200, headers: {'Content-Length': s}} ->
    console.log(`size is ${s}`),
  when {status: 404} ->
    console.log('JSON not found'),
  when {status} if (status >= 400) -> {
    throw new RequestError(res)
  },
}

case ... when ... 这样的代码就是模式匹配。

学生:模式匹配我大概了解了,就是高级版的 switch ... case。但是这个 Maybe 我还是不懂

方:在 Haskell 里,Maybe 的定义是

data Maybe a = Nothing | Just a -- 其中 a 可以是 Int / [Char] 等

作为参考,你可以看看 Haskell 里 Bool 的定义

data Bool = True | False

你不用在意关键字 data 是什么意思,你只需要用代入法即可,即

Maybe Int = Nothing | Just Int
Maybe [Char] = Nothing | Maybe [Char]

学生:那 Just "hi" 中的 Just 是什么?函数?还是类?

方:都不是,Just 就如同 True 或 False 一样,是特殊的值。Just "hi" 是一个整体,它不等于 "hi",其主要作用就是用来做模式匹配。

学生:那怎么从 Just "hi" 里取出 "hi" 呢?

方:你可以写一个 getValue

getValue :: Maybe [Char] -> [Char]
getValue (Just x) = x
getValue Nothing = error "无法读取值"

main = do 
  print $ getValue $ Just "hi"  -- 输出 "hi"

但这是一种非常不推荐的做法。

学生:那推荐做法是?

方:推荐「先不要把值从 Maybe 里取出来」,在真正需要用到值的时候用模式匹配即可:

main = do 
  let maybe = getContentFromFile "./1.txt"
  case maybe of
    Just x -> print $ "result: " ++ x
    Nothing -> print $ "we got nothing"

学生:我不太懂,我先取出来,得到一个 string,不是更方便吗?

方:JS 里确实是这样的,但是 Haskell 是一个支持惰性求值的语言。JS 不支持惰性求值还真不好解释,我举个另外的例子吧,如果 getContentFromFile 是异步操作,你怎么取出值?虽然你取不出值,但是你可以先把后续操作先写上去。

学生:你让我想到了 Promise

方:没错,promise 的值可能要 3 秒钟后返回,你不可能在那傻等 3 秒,不如趁这个时间把后续操作先写到 then 里。

let promise = readFilePromise("./1.txt")
promise.then(
  x => console.log("result: " + x),
  error => console.log("we go nothing")
)

有没有发现上面两段代码迷之相似?

学生:我怎么感觉,Maybe 就是同步的 Promise?Maybe<string> 表示可能有 string 也可能为空,Promise<User> 表示可能有 User 也可能为空。

方:有那么点意思,后面我们会发现它们的共通之处。

学生:JS 有了空,是不是就没有必要有 Maybe 类型了?

方:没错,就如同我们之前讲的「闭包和对象」一样,它们是殊途同归的,一门语言

  • 要么像 JS 那样,设 whatever 的类型为 string | undefined,你写 whatever.split('') 的时候不报错,运行
  • 要么像 TS 那样,设 whatever 的类型为 string | undefined,但是 whatever.split('') 报错,要求你先判空
  • 要么像 Haskell 那样,whatever 的类型为 Maybe [Char],也就是 Just [Char] | Nothing,你无法直接通过 whatever 调用 string 的 API,除非你用模式匹配拿到 string 值

三种方案都可以达到相同的目的,其中 JS 的做法最不安全,但新手最喜欢。TS 和 Haskell 的做法都安全,而且老手喜欢。

学生:原来学好编程要掌握这么多编程语言才行

方:没错!

未完待续……