精华内容
下载资源
问答
  • 如果你接触过函数式编程,你很可能遇到过 Monad 这个奇怪的名词。由于各种神奇的原因,Monad 成了一个很难懂的概念。Douglas Crockford 曾转述过这样一话来形容 Monad: Once you understand Monad, you lose the ...

    7f1372a92666c1caac668ebd6bc56cfc.png

    如果你接触过函数式编程,你很可能遇到过 Monad 这个奇怪的名词。由于各种神奇的原因,Monad 成了一个很难懂的概念。Douglas Crockford 曾转述过这样一句话来形容 Monad:

    Once you understand Monad, you lose the ability to explain it to someone else.

    这篇文章中,我会从使用场景出发来一步步推演出 Monad。然后,我会进一步展示一些 Monad 的使用场景,并解释一些我从 Haskell 翻译成 JS 的 ADT (Algebraic Data Type)。最后,我会介绍 Monad 在范畴论中的意义,并简单介绍下范畴论。

    函数组合

    1. Monoid

    假设你被一个奇怪的丛林部落抓住了,部落长老知道你是程序员,要你写个应用,写出来就放你走。作为一个资深码农,你暗自窃喜,心里想着老夫经历了这么多年产品经理各种变态需求的千锤百炼,没什么需求能难倒我!长老似乎看出了你的心思,加了一个要求:这个应用只能用纯函数写,不能有状态机,不能有副作用!然后你崩溃了……

    再假设你不知道函数式编程,但你足够聪明,你可能会发明出一个函数来满足这个奇葩的要求。这个函数如此强大,你可能会叫它超级函数,但其实它无可避免就是一个 Monad。

    接下来我们就来一步步推演出这个超级函数吧。

    函数组合大家都应该非常熟悉。比如,Redux 里面在组合中间件的时候会用到一个 compose 函数 compose(middleware1, middleware2)。函数组合的意思就是,在若干个函数中,依顺序把前一个函数执行的结果传个下一个函数,逐次执行完。compose 函数的简单实现如下:

    const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))
    

    函数组合是个很强大的思想。我们可以利用它把复杂问题拆解成简单问题,把这些简单问题逐个解决了之后,再把这些解决方案组合起来,就形成了最终的解决方案。

    这里偷个懒再举一下我之前文章的例子吧:

    // 在产品列表中找到相应产品,提取出价格,再把价格格式化
    const formalizeData = compose(
      formatCurrency,
      pluckPrice,
      findProduct
    )
    
    formalizeData(products)
    

    如果你理解了上面的代码,那么恭喜你,你已经懂了 Monoid!

    所谓 Monoid 可以简单定义如下:

    • 它是一个集合 S
    • S 的元素之间有一个二元运算 x,运算的结果也属于 S:S a x S b --> S c
    • 存在一个特殊元素 e,使得 S 中的任意元素与 e 运算,都返回此元素本身:S e x S m --> S m

    同时,这个二元运算要满足这些条件:

    • 结合律:(a x b) x c = a x (b x c), a,b,c 为 S 中元素
    • 单元律:e x a = a x e = a,e 为特殊元素,a 为 S 中任意元素

    注意,上面这个定义是集合论中的定义,这里还没涉及到范畴论。

    函数要能组合,类型签名必须一致。如果前一个函数返回一个数字,后一个函数接受的是字符串,那么是没办法组合的。所以,compose 函数接受的函数都符合如下函数签名:fn :: a -> a 也就是说函数接受的参数和返回的值类型一样。满足这些类型签名的函数就组成了 Monoid,而这个 Monoid 中的特殊元素就是 identity 函数:const identity = x => x; 结合律和单元律的证明比较简单,我就不演示了。

    2. Functor

    上面演示的函数组合看起来很舒服,但是实际用处还不是很大。因为 compose 接受的函数都是纯函数,只适合用来计算。而现实世界没有那么纯洁,我们要处理 IO,逻辑分支,异常捕获,状态管理等等。单靠简单的纯函数组合是不行的。

    先假设我们有两个纯函数:

    const addOne = x => x + 1
    const multiplyByTwo = x => 2 * x
    

    理想状态下是我们可以组合这两个函数:

    compose(
      addOne,
      multiplyByTwo
    )(2) // => 5
    

    但是我们出于各种原因要执行一些副作用。这里仅为了演示,就简单化了。假设上面两个函数在返回值之前还向控制台打印了内容:

    const impureAddOne = x => {
      console.log('add one!')
      return x + 1
    }
    
    const impureMultiplyByTwo = x => {
      console.log('multiply by two!')
      return 2 * x
    }
    

    现在这两个函数不再纯洁了,我们看不顺眼了。怎样让他们恢复纯洁?很简单,作弊偷个懒:

    const lazyImpureAddOne = x => () => {
      console.log('add one!')
      return x + 1
    }
    
    // Java 代码看多了之后我也学会取长变量名了^_^
    const lazyImpureMultiplyByTwo = x => () => {
      console.log('multiply by two!')
      return 2 * x
    }
    

    修改之后的函数,提供同样的参数,每次执行他们都返回同样的函数,可以做到引用透明。这就叫纯洁啊!

    然后我们可以这样组合这两个偷懒函数:

    composeImpure = (f, g) => x => () => f(g(x)())()
    
    const computation = composeImpure(lazyImpureAddOne, lazyImpureMultiplyByTwo)(8)
    
    computation() // => multiply by two!add one! 17
    

    在执行 computation 之前,我们都在写纯函数。

    我知道,我知道,上面的写法可读性很差。这样子写也不可维护。我们来写个工具函数方便我们组合这些不纯洁的函数:

    const Effect = f => ({
      map: g => Effect(x => g(f(x))),
      runWith: x => f(x),
    })
    
    Effect.of = value => Effect(() => value)
    

    这个 Effect 函数接受一个非纯函数 f 为参数,返回一个对象。这个对象里面的 map 方法把自身接受的非纯回调函数 g 和 Effect 的非纯回调函数组合后,将结果再塞回给 Effect。由于 map 返回的也是对象,我们需要一个方法把最终的计算结果取出来,这就是 runWith 的作用。

    Effect 重现我们上一步的计算如下:

    Effect(impureAddOne)
      .map(impureMultiplyByTwo)
      .runWith(2) // => add one!multiply by two! 6
    

    现在我们就可以直接用非纯函数了,不用再用那么难读的函数调用了。在执行 runWith 之前,程序都是纯的,任你怎么组合和 map

    如果你懂了上面的代码,那么恭喜你,你已经懂了 Functor!

    同样,Functor 还要满足一些条件:

    • 单元律:a.map(x => x) === a
    • 保存原有数据结构(可组合):a.map(x => f(g(x))) === a.map(g).map(f)
    • 提供接口往里面塞值:Effect.of = value => Effect(() => value)

    你可以把 Functor 理解成一个映射函数,它把一个类型里的值映射到同一个类型的其它值。比如数组操作 [1, 2, 3].map(String) // -> ['1', '2', '3'], 映射之后数据类型一样(还是数组),内部结构不变。

    3. Applicative

    上面的 Effect 函数把非纯操作都放进了一个容器里面,这样子做了之后,如果要对两个独立非纯操作的结果进行运算,就会很麻烦。

    比如,我们在 window 全局读取两个值 x, y, 并将读取结果求和。我知道这个例子很简单,不用函数式编程很容易做到,我只是在举简单例子方便理解。

    假设 window 对象已经存在两个值 {x: 1, y: 2, ...otherProps}。我们这样取:

    const win = Effect.of(window)
    
    const xFromWindow = win.map(g => g.x)
    
    const yFromWindow = win.map(g => g.y)
    

    xFromWindowyFromWindow 返回的都是一个 Effect 容器,我们需要给这个容器新添加一个方法,以便将两个容器里层的值进行计算。

    const Effect = f => ({
      map: g => Effect(x => g(f(x))),
      runWith: x => f(x),
      ap: other => Effect(x => other.map(f(x)).runWith()),
    })
    

    然后,我们提供一个相加函数 add:

    const add = x => y => x + y
    

    接下来借助这个 ap 函数,我们可以进行计算了:

    xFromWindow
      .map(add)
      .ap(yFromWindow)
      .runWith() // => 3
    

    由于这种先 map 再 ap 的操作很普遍,我们可以抽象出一个工具函数 liftA2:

    const liftA2 = (f, m1, m2) => m1.map(f).ap(m2)
    

    然后可以简化点写了:

    liftA2(add, xFromWindow, yFromWindow).runWith() // => 3;
    

    注意运算函数必须是柯里化函数。

    新增 ap 方法之后的 Effect 函数除了是 Functor,还是 Applicative Functor。这部分完全看代码还不是很好懂。如果你不理解上面的代码,没有关系,它并不影响你理解 Monad。另外,不用纠结于本文代码里的具体实现。不同的 Applicative 的 ap 方法实现都不一样,可以多看几个。Applicative 是介于 Functor 和 Monad 之间的数据类型,不提它就不完整了。

    Applicative 要满足下面这些条件:

    • Identity: A.of(x => x).ap(v) === v
    • Homomorphism: A.of(f).ap(A.of(x)) === A.of(f(x))
    • Interchange: u.ap(A.of(y)) === A.of(f => f(y)).ap(u)

    5. Monad (!!!)

    假设我们要从 window 全局读取配置信息,此配置信息提供目标 DOM 节点的类名 userEl;根据这个类名,我们定位到 DOM 节点,取出内容,然后打印到控制台。啊,读取全局对象,读取 DOM,控制台输出,全是作用,好可怕…… 我们先用之前定义的 Effect 试试看行不行:

    // DOM 读取和控制台打印的行为放进 Effect
    const $ = s => Effect(() => document.querySelector(s))
    const log = s => Effect(() => console.log(s))
    
    Effect.of(window)
      .map(win => win.userEl)
      .map($)
      .runWith() //由于上一个 map 里层也返回了 Effect,这里需要抹平一层
      .map(e => e.innerHTML)
      .map(log)
      .runWith()
      .runWith()
    

    勉强能做到,但是这样子先 maprunWith 实在太繁琐了,我们可以再给 Effect 新增一个方法 chain:

    const Effect = f => ({
      map: g => Effect(x => g(f(x))),
      runWith: x => f(x),
      ap: other => Effect(x => other.map(f(x)).runWith()),
      chain: g =>
        Effect(f)
          .map(g)
          .runWith(),
    })
    

    然后这样组合:

    Effect.of(window)
      .map(win => win.userEl)
      .chain($)
      .map(e => e.innerHTML)
      .chain(log)
      .runWith()
    

    线上 Demo 见这里

    Voila! 我们发现了 Monad!

    在写上面的代码的时候我还是觉得逐行解释代码比较繁琐。我们先不管代码具体实现,从函数签名开始看 Monad 是怎么回事。

    让我们回到 Monoid。我们知道函数组合的前提条件是类型签名一致。fn :: a -> a. 但在写应用时,我们会让函数除了返回值之外还干其他事。这里不管具体干了哪些事,我们可以把这些行为扔到一个黑盒子里(比如刚刚写的 Effect),然后函数签名就成了 fn :: a -> m a。m 指的是黑盒子的类型,m a 意思是黑盒子里的 a. 这样操作之后,Monoid 接口不再满足,函数不能简单组合。

    但我们还是要组合。

    其实很简单,在组合之前把黑盒子里的值提升一层就行了。最终我们实现的组合其实是这样:fn :: m a -> (a -> m b) -> m b. 这个签名里,函数 fn 接受黑盒子里的 a 为参数,再接受一个函数为参数,这个函数的入参类型是 a,返回类型是黑盒子里的 b。最终,外层函数返回的类型是黑盒子里的 b。这个就是 chain 函数的类型签名。

    fn :: a -> m a 签名里面的箭头叫 Kleisli Arrow,其实就是一种特殊的函数。Kleisli 箭头的组合叫 Kleisli Composition,这也是 Ramda 里面 composeK 函数的来源。这里先了解一下,等下还会用到这个概念。

    Monad 要满足的一些定律如下:

    • Left identity: M.of(a).chain(f) === f(a)
    • Right identity: m.chain(M.of) === m
    • Associativity: m.chain(f).chain(g) === m.chain(x => f(x).chain(g))

    很多人误解 JS 里面的 Promise 就是个 Monad,我之前也有这样的误解,但后来想明白了。按照上面的定律来看检查 Promise:

    Left identity:

    Promise.resolve(a).then(f) === f(a)
    

    看起来满足。但是如果 a 是个 Promise 呢?要处理 Promise,那 f 应该符合符合这个函数的类型签名:

    const f = p => p.then(n => n * 2)
    

    来试一下:

    const a = Promise.resolve(1)
    const output = Promise.resolve(a).then(f)
    // output :: RejectedPromise TypeError: p.then is not a function
    

    报错的原因是,a 在传给 f 之前,就已经被 resolve 掉了。

    Right identity:

    p.then(x => Promise.resolve(x)) === p
    

    满足。

    Associativity:

    p.then(f).then(g) === p.then(x => f(x).then(g))
    

    和左单元律一样,只有当 f 和 g 接受的参数不为 Promise,上面才成立。

    所以,Monad 的三个条件,Promise 只符合一条。

    更多 ADT

    上面演示的 Effect 函数,和我之前文章《不完整解释 Monad 有什么用》 里面演示的 IO 函数是同一个 ADT,它是用来处理程序中的作用的。函数式编程中还有很多不同用处的 ADT,比如,处理异步的 Future,处理状态管理的 State,处理依赖注入的 Reader 等。关于为什么这个 Monad 是代数数据类型,Monad 和大家熟知的代数有什么关系,这里不展开了,有兴趣进一步了解的话可以参考 Category Theory for Programmers 这本书。

    这里再展示两个 ADT,Reader 和 State,比较它们 chain 和 ap 的不同实现,对比 Monadic bind 函数类型签名 chain :: m a -> (a -> m b) -> m b,思考下它们是怎样实现 Monad 的。

    1. Reader

    const Reader = computation => {
      const map = f => Reader(ctx => f(computation(ctx)))
    
      const contramap = f => Reader(ctx => computation(f(ctx)))
    
      const ap = other => Reader(ctx => computation(ctx)(other.runWith(ctx)))
    
      const chain = f => {
        return Reader(ctx => {
          const a = computation(ctx)
          return f(a).runWith(ctx)
        })
      }
    
      const runWith = computation
    
      return Object.freeze({
        map,
        contramap,
        ap,
        chain,
        runWith,
      })
    }
    
    Reader.of = x => Reader(() => x)
    

    题外话补充下,上面这种叫“冰冻工厂”的工厂函数写法,是我个人偏好。这样写会有一定性能和内存消耗问题。用 Class 性能更好,看你选择。

    程序中可能会遇到某个函数对外部环境有依赖。用纯函数的写法,我们可以把这个依赖同时传进函数。这样子,函数签名就是 fn :: (a, e) -> b。e 代表外部环境。这个签名不符合我们前面提到的 a -> m b. 我们到现在还只提到了一次函数柯里化,这个时候再一次要用柯里化了。柯里化后,有依赖的函数类型签名是 fn :: a -> (e, b), 你可能认出来了,中间那个箭头就是 Kleisli Arrow。

    假设我们有一段程序的多个模块依赖了共同的外部环境。要做到引用透明,我们必须把这个环境传进函数。但是每一个模块如果都接受外部环境为多余参数,那这些模块是没办法组合的。Reader 帮我们解决这个问题。

    来写个简单程序,执行这个程序时输出“你好,xx ... 再见,xx”。xx 由执行时的参数决定。

    const concat = x => y => y.concat.call(y, x)
    
    const greet = greeting => Reader(name => `${greeting}, ${name}`)
    
    const addFarewell = farewell => str =>
      Reader(name => `${str}${farewell}, ${name}`)
    
    const buildSentence = greet('你好')
      .map(concat('...'))
      .chain(addFarewell('再见'))
    
    buildSentence.runWith('张三')
    // => 你好, 张三...再见, 张三
    

    上面这个例子过于简单。输出一个字符串用一个函数就行,用不了解构和组合。但是,我们可以很容易扩展想象,如果 greetaddFarewell 是很复杂的模块,必须拆分,此时组合的价值就出现了。

    在学习 Reader 时,我发现一篇很不错的文章。这篇文章大开脑洞,用 Reader 实现 React 里面的 Context。有兴趣可以了解下。The Reader monad and read-only context

    2. State

    // 这个写法你可能不习惯。
    // 这是 K Combinator,Ramda 里面对应函数是 always, Haskell 里面是 const
    const K = x => y => x
    
    const State = computation => {
      const map = f =>
        State(state => {
          const prev = computation(state)
          return { value: f(prev.value), state: prev.state }
        })
    
      const ap = other =>
        State(state => {
          const prev = computation(state)
          const fn = prev.value
          return other.map(fn).runWith(prev.state)
        })
    
      const chain = fn =>
        State(state => {
          const prev = computation(state)
          const next = fn(prev.value)
          return next.runWith(prev.state)
        })
    
      const runWith = computation
    
      const evalWith = initState => computation(initState).value
    
      const execWith = initState => computation(initState).state
    
      return Object.freeze({
        map,
        ap,
        chain,
        evalWith,
        runWith,
        execWith,
      })
    }
    
    const modify = f => State(state => ({ value: undefined, state: f(state) }))
    
    State.get = (f = x => x) => State(state => ({ value: f(state), state }))
    
    State.modify = modify
    
    State.put = state => modify(K(state))
    
    State.of = value => State(state => ({ value, state }))
    

    State 里层最终返回的值由对象构成,对象里面包含了此时计算结果,以及当前的应用状态。

    再举个简单的例子。假设我们根据某状态数字进行计算,首先我们在这个初始状态上加某个数字,然后我们把状态 + 1, 再把新的状态和前一步的计算相乘,算出最终结果。同样,例子很简单,但已经包含了状态管理的核心。来看代码:

    const add = x => y => x + y
    
    const inc = add(1)
    
    const addBy = n => State.get(add(n))
    
    const multiplyBy = a => State.get(b => b * a)
    
    const incState = n => State.modify(inc).map(K(n))
    
    addBy(10)
      .chain(incState)
      .chain(multiplyBy)
      .runWith(2) // => {value: 36, state: 3}
    

    上面最后一步组合,每个函数类型签名一致,a -> m b, 构成 kleisli 组合,我们还可以用工具函数改进一下写法:

    const composeK = (...fns) =>
      fns.reduce((f, g) => (...args) => g(...args).chain(f))
    
    const calculate = composeK(
      multiplyBy,
      incState,
      addBy
    )
    
    calculate(10).runWith(2) // => {value: 36, state: 3}
    

    范畴论介绍

    Monad 有一个“臭名昭著”的定义,是这样:

    A monad is just a monoid in the category of endofunctors, what's the problem?

    我见过这句话的中文翻译。但是这种“鬼话”不管翻不翻译都差不多的表达效果,我觉得还是不用翻译了。很多人看到这句话不去查出处和上下文,就以此为据来批评 FP 社区故弄玄虚,我感到很无奈。

    这句话出自这篇文章 Brief, Incomplete and Mostly Wrong History of Programming Languages. 这篇文章用戏谑调侃的方式把所有主流编程语言黑了一个遍。上面那句话是用来黑 Haskell 的。本来是句玩笑,结果就以讹传讹了。

    上面那句话的原始出处是范畴论的奠基之作 Categories for the Working Mathematician 原话更拗口:

    All told, a monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the identity endofunctor.

    注意书名,那是给数学家看的,不是给程序员看的。你看不懂很正常,看不懂还要骂这些学术泰斗装逼就是你的不对了。

    范畴论背景

    首先,说明下我数学学得差,我接下来要讲的名词我知道是在研究什么,再深入细节我就不知道了。

    大家知道数学有很多分支,比如集合论,逻辑学,类型论(Type Theory) 等等。后来,有些数学家发现,如果用足够抽象的概念工具去考察这些分支,其实他们都在讲同样的东西。桥接这些概念的工具是 isomorphism (同构)。isomorphic 就是在对象之间可以来回转换,每次转换没有信息丢失。比如,在逻辑学里面研究的某个问题,可能和类型论里面研究是同一个问题,只要两者之间能形成 isomorphism。

    统一数学各分支的理论就是范畴论。范畴论需要足够抽象,避免细节,才能在相差巨大的各数学分支之间发现同构。这也是为什么范畴论必须要用一些生僻的希腊词根合成词。因为它实在太抽象了,很难找到现有的词汇去对应它里面的一些概念。混用词汇肯定会导致误解。

    再后来,FP 祖师爷之一 Haskell Curry,和另一个数学家一起发现了 Curry–Howard Isomorphism。这个理论证明了 proofs as programs, 就是说写电脑程序(当然是函数式)和写逻辑证明是一回事,两者形成同构。再后来,这个理论被扩展了一下,成了 Curry–Howard-Lambek Isomorphism, 就是说逻辑学,程序函数,和范畴论,三者之间形成同构。

    看了上面的理论背景,你应该明白了为什么函数式编程要从范畴论里面获取理论资源。

    什么是范畴 (Category)

    范畴其实是很简单的一个概念。范畴由一堆(这个量词好难翻译,我见过 a bunch, a collection, 但是不能说 a set)对象,以及对象之间的关系构成。我分两部分介绍。

    对象 (Object): 范畴论里面的对象和编程里面的对象是两回事。范畴中的对象没有属性,没有结构,你可以把它理解为不可描述的点。

    箭头 (arrow, morphism, 两个词说的是同一个东西, 我后面就用箭头了): 连接对象,表示对象之间的关系。同样,箭头也是一个没有结构没有属性的一种 primitive。它只说明了对象之间存在关系,并不能说明是什么关系。

    对象和箭头要构成一个范畴,还要满足这两个条件:

    • 单元律。每个对象至少有一个箭头能从自己出发回到自身。
    • 结合律。如果对象 a 和 b 之间存在箭头 f,对象 b 和 c 之间存在箭头 g,则必然存在箭头 h 由 a 到 c,h 就是 f 和 g 的组合。

    可以看出范畴论的起点真的非常简单。很难想象基于这么简单的概念能构建出一个完整的数学理论。

    我一开始试着在范畴论中来解释 Monad,以失败告终。要介绍的拗口名词太多了,一篇文章根本讲不完。所以本文会折中一下,还是用集合论的视角来解释一下范畴论概念。(范畴论的单个对象可以对应成一个集合,但是范畴论禁止谈论集合元素,所有关于对象的知识都由箭头和组合推理出来,所以很头疼。)

    勘误:以下内容是我在仓促学了范畴论知识后想当然的推断,不够准确,请参考评论区讨论。目前我暂无精力重学重写,见谅。

    还记得我们是用集合来定义 Monoid 的吧?Monoid 其实就是一个只有一个对象的范畴。范畴和范畴之间的映射叫 Functor。如果一个 Functor 把范畴映射回自身,那么这个 Functor 就叫 Endofunctor。Functor 和 Functor 之间的映射叫 Natural Transformation. 函数式编程其实只处理一个范畴,就是数据类型(Types)。所以,我们前面提到的 Functor 也是 Endofunctor。

    回到前面 Monad 中 chain 的类型签名:

    chain :: m a -> (a -> m b) -> m b

    可以看出 Monad 是把一个类型映射回自身(m a -> m b),那么它就是一个 Endofunctor。

    再看看 Monad 中所运用的 Natural Transformation。还是看 chain 的签名,前半部分 m a -> (a -> m b) 执行之后,类型签名是 m (m b), 然后再和后面的连起来,就是 m (m b) -> m b. 这其实就是把一个 functor (m (m b)) 映射到另一个 Functor (m b)。m (m b) -> m b 看起来是不是很眼熟?一个 Functor 和自己组合,形成同一个范畴里的 Functor,这种组合就是 Monoid 啊!我们一开始定义的 Monoid 中的二元运算,在 Monad 中其实就是 Natural Transformation。

    那么,再回到这一部分开始时的定义:

    A monad is just a monoid in the category of endofunctors.

    有没有好理解一点?

    为什么要这样写程序

    这篇文章的目的不是鼓励你在你的代码中消灭状态机,消灭副作用,我自己都做不到的。我司后端是用 Java 写的,如果我告诉后端同事 “Yo,你的程序里不能出现状态机哦……”,怕是会被哄出办公室的。那么,为什么要了解这些知识?

    计算机科学中有两条截然相反的路径。一条是自下而上,从底层指令开始往上抽象(优先考虑性能),逐渐靠近数学。比如,一开始的 Unix 操作系统是用汇编写的,后来发现用汇编写程序太痛苦了,需要一些抽象,所以出现了高级语言 C,再后来由于各种编写应用的需求,出现了更高级的语言如 Python 和 JavaScript。另一条路径是自上而下的,直接从数学开始(Lambda 演算),不考虑性能和硬件状况,按需逐渐减少抽象。前一条路径明显占了主流,代表语言是 Fortran, C, C++, Pascal, 和 Java 等。后面一条路径不够实用,比较小众,代表语言是 Algo, LISP, 和 Haskell 等。

    这两个阵营肯定是有争论的。前者想劝后者从良:你别扔给我这么多函数,我没法不影响性能情况下处理那么多垃圾回收和函数调用!后者也想叫醒前者:不要过早深入硬件细节,你会把自己锁定在无法逆转的设计错误上!两者分道扬镳了 60 多年,这些年总算开始融合了。比如,新出现的程序语言如 Scala,Kotlin,甚至系统编程语言 Rust,都大量借鉴了函数式编程的思想。

    学些高阶抽象还能帮助你更容易理解一些看起来很复杂的概念。转述一个例子。C++ 编程里面最高的抽象是模板元编程(Template Meta Programming),据说很难懂。但是据 Bartosz Milewski 的解释,之所以这个概念难懂,是因为 C++ 的语言设计不适合表达这些抽象。如果你会 Haskell,就会发现其实一行代码就完成了。


    参考:

    Brian Beckman: Don't fear the Monad

    What Does Haskell Have to Do with C++?

    HOW TO DEAL WITH DIRTY SIDE EFFECTS IN YOUR PURE FUNCTIONAL JAVASCRIPT

    Category Theory for Programmers

    No, Promise is not a monad

    展开全文
  • 数理逻辑—命题符号化及联结词

    千次阅读 2019-04-23 23:25:47
    对命题有关概念的部分名词解释: 命题的真值:即判断的可能结果,“真"与"假” 真命题:真值为真的命题 假命题:真值为假的命题 真值的取值:即"真"或"假"其中之一 命题常项与变项 命题常项与变项的定义:...

    命题

    命题的定义:
    1. 能判断真假的陈述句为命题
    2. 命题是具有唯一真值的陈述句

    从以上两个定义可知,判断一个句子是否为命题,首先要看它是否为陈述句,然后在看它的真值是否唯一。

    对命题有关概念的部分名词解释:

    1. 命题的真值:即判断的可能结果,“真"与"假”
    2. 真命题:真值为真的命题
    3. 假命题:真值为假的命题
    4. 真值的取值:即"真"或"假"其中之一
    命题常项与变项

    命题常项与变项的定义:
    对于简单命题来说,当它的真值是确定的,就称该命题为命题常项(命题常元);当它的真值是不确定的,就称该命题为命题变项(命题变元)

    简单命题(原子命题)的定义:
    无法再分解为更简单陈述句的陈述句,即最简单的陈述句。例如:“2是素数”、“雪是黑色的”。

    命题符号化与命题常项与变项举例:
    为了方便表达,通常会使用英文小写字母来表示简单命题。
    命题常项举例:

    • pp:2是素数.
    • qq:雪是黑色的.

    此时的ppqq就可以称为命题常项。
    显然有,命题常项pp是真命题,命题常项qq是假命题。

    命题变项举例:

    • ppx+y>5x+y>5

    类似这种真值不确定的陈述句就称为命题变项,通常也用小写字母表示。但是需要注意的是,命题变项不是命题。

    复合命题:

    由简单命题用联结词联结而成的命题称为复合命题。复合命题是命题逻辑的主要研究对象。

    例如:

    1. 3不是偶数:¬p\lnot p
    2. 2是素数和偶数:pqp\land q
    3. 他会说英语或日语:pqp\lor q
    4. 若∠A与∠B是对顶角,则∠A等于∠B:(pq)r(p\land q)\to r

    联结词(逻辑运算符)

    联结词的种类:

    设有命题ppqq,它们之间可用的联结词按优先级从高到第在表格中进行说明:

    联结词 符号 名称 含义
    否定词 ¬p\lnot p pp 表达对pp的否定
    合取词 pqp\land q pp合取qq 表达ppqq
    析取词 pqp\lor q pp析取qq 表达ppqq
    蕴含词 pqp\to q pp蕴含qq 表达如果ppqq
    等价词 pqp \leftrightarrow q pp等价qq 表达pp当且仅当qq
    与非词 pqp\uparrow q ppqq的否定 ¬(pq)\lnot(p\land q)
    或非词 pqp\downarrow q ppqq的否定 ¬(pq)\lnot(p\lor q)
    联结词的完备集(全功能集)

    完备集的定义:SS是一个联结词集合,若任一真值函数都可以用仅含SS中的联结词的命题公式表示,则称SS为完备集(全功能集)

    完备集:
    {¬,,}\{\lnot,\land,\lor\}{¬,}\{\lnot,\land\}{¬,}\{\lnot,\lor\}{¬,}\{\lnot,\to\}{}\{\uparrow\}{}\{\downarrow\}

    展开全文
  • 【托福】阅读

    2020-02-10 15:46:52
    【托福】阅读 做题步骤: 扫读题目,判断题型 词汇题 是否认识? 熟词:熟词僻义(意思不同,但同为贬义或者褒义,...举例 除去for example,such as等,还应注意概述(数量词+名词复数),概述后面一般会举例...

    【托福】阅读

    在这里插入图片描述

    做题步骤:
    扫读题目,判断题型

    词汇题
    是否认识?
    熟词:熟词僻义(意思不同,但同为贬义或者褒义,优先检查) 熟词熟义(近义,带入检查) 正确答案=这个单词本身意思+单词上下文语境中的含义
    生词:看原词前后两句,根据句子关系找对应选项

    1. 解释关系:破折号、冒号、括号、分号等
      举例 除去for example,such as等,还应注意概述句(数量词+名词复数),概述句后面一般会举例。
      从句 通过从句推断处意思
      代词 所考词汇之前出现代词,向前寻找与所考词汇词性相同的同义改写
      threadlike = fibrous 线状的
    2. 句间关系:通过句子与句子之间的关系判断
      并列:A and B,A和B角度类似
      转折:A but B,A和B角度不同
    3. 固定搭配:动宾关系—利用宾语推导动词含义,但是反之不可行
      比如gratify all of their demands ,可以利用demands推导gratify。
    4. 感情色彩 lament = complained about 令人遗憾的
    5. 词根词缀:单词意思往往和词根词缀有关 pro- 向前的 puls推动

    细节题

    1. 大定位:定位到某个自然段
      要点:题目的段号/箭头;行文顺序与出题顺序基本一致;选项是不按行文顺序出的;
    2. 小定位:缩小定位区间至1-2句话
      要点:寻找定位词

    定位词的特征

    • 大写人名、地点、时间before/after等、数字、年代等
    • 不容易被paraphrase的名词例如学科性词汇
    • 在原文中重现次数比较少的词,标题词汇不太容易成为定位词

    哪里找定位词?一先题干,二再选项,若无法找,回大定位

    1. 回大定位:有些题目难以迅速找到定位词,可以回到原大定位区间,每读一两句排除选项

    细节题正确答案特征:

    1. 概括、抽象
    2. 永远是原文的同义置换

    推断题
    题干有 imply/infer/most likely/probably/suggest…
    思路与细节题一致,应当指出基于原文哪里的语言作出推断

    排除列举题
    思路与细节题一致
    little wealth 没钱

    句子简化题
    错误选项:

    1. 与原句相矛盾
    2. 漏掉重要信息

    要点:

    • 逻辑关系不能变
    • 论述对象不可变
    • 论述内容不能变

    句子插入题
    指代关系
    逻辑衔接
    指代对象不变性

    1. 分析插入句中的线索
    2. 回到原文阅读,不需要读全文
    3. 代入原文分析前后内容匹配程度

    指代题

    • 指代传递指代词通常首先指前一句的主语,其次是宾语,当前一句主语也是代词时,则继续向前找。
    • some/others结构中,后半部分往往指代前半部分后面紧跟的名词成分
    • 关系代词that,which,what等通常就近指代,前提是去掉插入语

    修辞目的题
    例证目的:阅读关键词所在句子,寻找对应主题句,阅读选项
    段落目的和结构目的:寻找对应主题句,阅读选项

    展开全文
  • 陈述就是:通过看GI值来判断该食物能不能在减肥期间吃,没有意义。wait!!你都点进来了,不如看下去,就当增长一丢丢知识也好的呀~不减肥的小伙伴可能连GI是什么都不太清楚~那咱们就先从解释名词开始!GI值是...

    ​首先!

    标题即答案!

    是的你没看错!

    陈述句就是:

    通过看GI值来判断该食物能不能在减肥期间吃,没有意义。

    e76cfe917c4a6bc0e7b32a4b34ff022a.png

    wait!!你都点进来了,不如看下去,就当增长一丢丢知识也好的呀~

    0c7b40803374981bc81058afaba3ce9f.png

    不减肥的小伙伴可能连GI是什么都不太清楚~

    那咱们就先从解释名词开始!

    GI值是什么

    GI(Glycemic Index)即升糖指数。

    食物的GI值越高,吃进去的东西越容易变成脂肪存储在体内 & 越容易饿!

    而且这东西是个“恶性循环”,会让你越吃越多,越吃饿越快!

    高GI >=70; 中GI:56~69; 低GI<=55

    既然如此,那为什么又要说看GI减肥毫无意义呢?

    深入理解一下GI

    GI是指摄入含有 100g碳水化合物(不包括纤维)的该食物后,身体 两小时后的血糖上升水平。

    啥意思呢,咱们拿南瓜举例:

    44e9122f7c35846b5297effb5bee9024.png

    上图显示南瓜的GI值是75。

    照理说是高GI值了,但为啥还总被推荐代替主食呢?

    注!GI是指含有100g碳水化合物的食物,而不是单纯 你吃了100g这个食物。

    这样就好理解了,该图显示南瓜每100g的碳水化合物是5.3g,所谓的75的GI值,实则是指你吃了1887g南瓜之后你身体的升糖水平。(但谁能一下子吃1887g南瓜。。。>

    所以按照平时的吃法,根本产生不到这么高的GI。

    而且,GI值在每个人身体中的效果都是不一样的,标注的GI值只是一个平均值。

    另外,根据你的烹饪手段,GI也会有明显的不同。

    油炸,红烧等方式,会大大提高食物的GI值。

    然而 营养素的多样性还能降低GI值~

    8acbba94d83f8694089b068abde0b2ff.png

    b358e9f29d94a61c13c39854a1fd9778.png

    从这张图来看,面条的GI低于米饭很多。

    这时候很多不理解GI的人就会想. “噢!减肥吃面条!” 然后越吃越胖。

    首先,咱们吃米饭是用来配菜的,没人会干吃白饭吧。搭配蔬菜,肉类。同时拥有了膳食纤维,蛋白质的摄入,就会将GI值降低。

    但大家一般都是怎么吃面条的,炒面?拌面?汤面?总之,吃面条就是吃面条,即使有蔬菜,肉类,也都是为辅,挑不起大梁。

    这样一对比,面条GI分分钟高过米饭。

    6350b1ffd16f0d1b34c5410a8818f084.png

    既然GI值没有意义。那是不是就可以不管了。

    答案是:

    你可以看GL值呀!GL值=该食物中的碳水化合物含量 x GI值

    GL是GI的高阶玩家,同样身为高阶玩家的你,减脂期间可以多看看GL值~

    GL>20的为高GL食物;

    GL在10~20之间的为中GL食物;

    GL<10的为低GL食物。

    在吃同类食物时,选择更低GL值的食物,更加利于减肥噢~

    展开全文
  • JAVA 正则表达式

    热门讨论 2010-01-15 11:16:37
    众所周知,在程序开发中,难免会遇到需要匹配、查找、替换、判断字符串的情况发生,而这些情况有时 又比较复杂,如果用纯编码方式解决,往往会浪费程序员的时间及精力。因此,学习及使用正则表达式, 便成了解决这...
  • java面试题典 java 面试题 经典

    热门讨论 2010-06-18 13:42:36
    43. WEB SERVICE名词解释。JSWDL开发包的介绍。JAXP、JAXM的解释。SOAP、UDDI,WSDL解释。 26 三、 JSP 27 1. JSP中动态INCLUDE与静态INCLUDE的区别? 27 2. JSP的内置对象及方法。 27 3. JSP的常用指令 27 4. 页面间...
  • 最新Java面试宝典pdf版

    热门讨论 2011-08-31 11:29:22
    6、请对以下在J2EE中常用的名词进行解释(或简单描述) 129 7、如何给weblogic指定大小的内存? 129 8、如何设定的weblogic的热启动模式(开发模式)与产品发布模式? 129 9、如何启动时不需输入用户名与密码? 130 10、...
  • Java面试宝典2010版

    2011-06-27 09:48:27
    6、请对以下在J2EE中常用的名词进行解释(或简单描述) 7、如何给weblogic指定大小的内存? 8、如何设定的weblogic的热启动模式(开发模式)与产品发布模式? 9、如何启动时不需输入用户名与密码? 10、在weblogic管理制...
  • Java面试宝典-经典

    2015-03-28 21:44:36
    6、请对以下在J2EE中常用的名词进行解释(或简单描述) 129 7、如何给weblogic指定大小的内存? 129 8、如何设定的weblogic的热启动模式(开发模式)与产品发布模式? 129 9、如何启动时不需输入用户名与密码? 130 10、...
  • Java面试宝典2012版

    2012-12-03 21:57:42
    69、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这话对不对? 48 70、TreeSet里面放对象,如果同时放入了父类和子类的实例对象,那比较时使用的是父类的compareTo方法,还是使用的子类的...
  • java面试宝典2012

    2012-12-16 20:43:41
    69、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这话对不对? 52 70、TreeSet里面放对象,如果同时放入了父类和子类的实例对象,那比较时使用的是父类的compareTo方法,还是使用的子类的...
  • 2012版最全面试题大全目录: ...6、请对以下在J2EE中常用的名词进行解释(或简单描述) 129 7、如何给weblogic指定大小的内存? 129 8、如何设定的weblogic的热启动模式(开发模式)与产品发布模式? 129 9、如何启动时不...
  • Java面试笔试资料大全

    热门讨论 2011-07-22 14:33:56
    6、请对以下在J2EE中常用的名词进行解释(或简单描述) 129 7、如何给weblogic指定大小的内存? 129 8、如何设定的weblogic的热启动模式(开发模式)与产品发布模式? 129 9、如何启动时不需输入用户名与密码? 130 10、...
  • Java面试宝典2012新版

    2012-06-26 19:20:00
    一. Java基础部分 7 ...6、请对以下在J2EE中常用的名词进行解释(或简单描述) 129 7、如何给weblogic指定大小的内存? 129 8、如何设定的weblogic的热启动模式(开发模式)与产品发布模式? 129 9、如何启动...
  • JAVA面试宝典2010

    2011-12-20 16:13:24
    6、请对以下在J2EE中常用的名词进行解释(或简单描述) 129 7、如何给weblogic指定大小的内存? 129 8、如何设定的weblogic的热启动模式(开发模式)与产品发布模式? 129 9、如何启动时不需输入用户名与密码? 130 10、...
  • 6、请对以下在J2EE中常用的名词进行解释(或简单描述) 129 7、如何给weblogic指定大小的内存? 129 8、如何设定的weblogic的热启动模式(开发模式)与产品发布模式? 129 9、如何启动时不需输入用户名与密码...

空空如也

空空如也

1 2 3
收藏数 45
精华内容 18
关键字:

判断句名词解释