JavaScript真函数式编程

By guenchi at 2017-12-17 • 0人收藏 • 317人看过

http://jimliu.net/2015/10/21/real-functional-programming-in-javascript-1/

http://jimliu.net/2015/10/22/real-functional-programming-in-javascript-2/


没错,这篇文章是函数式装逼系列(1)(2)的进阶版。函数式编程是很火的,然而现在网上很多入门级的JS函数式编程教程(甚至书)都太水了,以为用用forEach用用map用用reduce用用immutable就叫函数式编程了。

Too young. Too simple.

本着搞一个大新闻的目的,我又开了这个无底天坑。当然一切都是从学习与娱乐的目的出发(这两件事其实并不冲突嘛),请勿评论本文中代码的实用价值。

瑞迪?黑喂狗!

0. 开篇

背景

要实现“真·函数式编程”,就要玩的彻底一点,必须挥刀自宫先禁用JavaScript里的大量语言特性,目前我能想到的有:

  1. 禁用var/let,所有东西都用const定义,也就是说无变量,强制immutable。

  2. 禁用分号,也就是不让“顺序执行”,解除过程式编程范式你不是不爱写分号吗,这次彻底不需要写了

  3. 禁用if/else,但允许用条件表达式condition ? expr1 : expr2。

  4. 禁用for/while/do-while。

  5. 禁用prototype和this来解除JS中的面向对象编程范式

  6. 禁用function和return关键字,仅用lambda表达式来编程(JS里叫箭头函数)。

  7. 禁用多参数函数,只允许使用单个参数,相当于强行curry化

但是为了更可读(其实也可读不到哪去),我们允许使用大量ES6的新特性,比如箭头函数(lambda表达式)、解构、参数解构等等。如果你想玩这些特性,建议使用最新版的Firefox,或者node.js 4.0或更高版本加上--harmony --harmony_modules --harmony_destructuring等参数。

因为文中会用到一些ES6的特性,可能有的同学还不太清楚,所以这里花一小点篇幅简单的挑重点介绍一下:

箭头函数

其他语言里面一般叫做lambda表达式,其实我个人当然是喜欢这个名字,但是因为ES6的语言规范里就把它管叫箭头函数,自然文中还是会尽量这么说。

箭头函数的基本定义方式是:

(参数列表) => {函数体}

当只有一个参数的时候,可以省略括号,写成

参数名 => {函数体}

当函数体是一个表达式(而不是段落)的时候,可以隐式return,写成

参数名 => 返回表达式

由于我们的“真·函数式编程”是禁用过程式编程的,不存在段落,于是你可以见到下文中几乎所有的箭头函数都是最简单的形式,例如x => x * 2。

箭头函数可以返回函数,并且在返回函数的时候,它也可以隐式return,因此可以像haskell一样构建curry风格的函数,如

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

用传统的风格来“翻译”上面的add函数,就是

function add(x) {return function(y) {return x + y}}

调用的时候自然也是使用curry风格的逐个传参add(5)(3),结果就是8。

解构

解构是现代编程语言当中一个非常非常甜的语法糖,有时候我们为了实现多返回值,可能会返回一个数组,或者一个KV,这里以数组为例

const pair = a => b => [a, b]

我们可以用解构一次性将数组中的元素分别赋值到多个变量如

const [a, b] = pair('first')('second') 
// a是'first',b是'second'

参数结构就是在定义函数参数的时候使用结构

const add = ([a, b]) => a + badd([5, 3])

在add函数里面,数组[5, 3]可以被自动解构成a和b两个值。数组解构有一个高级的“剩余值”用法:

const [first, ...rest] = [1, 2, 3, 4, 5] 
// first是1,rest是[2, 3, 4, 5]

可以把“剩余值”解构到一个数组,这里叫rest。

关于解构语法的更多趣闻,可以看看我之前的一篇博客


OK,前戏就到这里,下面进入主题。

1. 实现循环

实现for循环遍历数组

命令式编程当中,循环是最基本的控制流结构之一了,基本的for循环大概是这样:

for (var i = 0; i < arr.length; i++) {// ...}

看见了var i和i++了吗?因为不让用变量,所以在“真·函数式编程”当中,这样做是行不通的。

函数式语言当中使用递归实现循环。首先拆解一下循环的要素:

for (初始化; 终止条件; 迭代操作) {迭代体}

使用递归来实现的话,无外乎也就是把迭代终止换成递归终止。也就是说,只要有上面4个要素,就可以构造出for循环。首先将问题简化,我们只想遍历一个数组,首先定义一个迭代函数loop

function loop_on_array(arr, body, i) {
        if (i < arr.length) {
            body(arr[i])loop_on_array(arr, body, i + 1)
        }
    }

上面的函数有几个地方不满足“真·函数式编程”的需要:

  1. 使用了function定义:这个最简单,改成箭头函数就行了

  2. 使用了多个参数:这个可以简单的通过curry化来解决

  3. 使用了if/else:这个可以简单的通过条件表达式来解决

  4. 使用了顺序执行,也就是body(i)和loop(arr, body, i + 1)这两句代码使用了顺序执行

为了解除顺序执行,我们可以使用像“回调函数”一样的思路来解决这个问题,也就是说让body多接收一个参数next,表示它执行完后的下一步操作,body将会把自己的返回值以参数的形式传给next

const body = item =>
    next =>
        next(do_something_with(item))

这样需要修改body是不爽的,因此可以将其进行抽象,我们写一个two_steps函数来组合两步操作

const two_steps = step1 =>
    step2 =>
        param =>
            step2(step1(param))

这样,上面的两行顺序执行代码就变成了

two_steps (body) (_ =>
    loop_on_array(arr, body, i + 1)) (arr[i])

注意中间那个参数它是一个函数,而并不是直接loop(arr, body, i + 1),它所接收的是body(arr[i])的结果,但是它并不需要这个结果。函数式语言当中常常用_来表示忽略不用的参数,我们的“真·函数式编程”也会保留这个习惯。

这样通过two_steps来让两步操作能够顺序执行了,我们可以完成遍历数组的函数了

const loop_on_array = arr =>
    body =>
        i =>
            i < arr.length ?two_steps (body) (_ =>
                loop_on_array(arr)(body)(i + 1)) (arr[i]) :undefined

调用的时候就是

loop_on_array ([1, 2, 3, 4, 5]) (item => console.log(item)) (0)

但是你会发现最后那个(0)其实是很丑的对吧,毕竟它总是0,还不能省略,所以我们还是可以通过构造一个新的函数来抽取递归内容

const _loop = arr =>
    body =>
        i =>
// 原来的loop_on_array的内容
const loop_on_array = arr =>
    body =>
        _loop(arr)(body)(0)
// 调用
loop_on_array ([1, 2, 3, 4, 5]) (item => console.log(item))


实现map

在上面的遍历的代码里,我们用for循环的套路来实现了对一个数组的遍历。这个思想其实还不算特别functional,要让它逼格更高,不妨从map这个函数来考虑。

map就是把一个数组arr通过函数f映射成另一个数组result,在Haskell里面map的经典定义方式是

map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = f x : map f xs

简单的说就是:

  1. 对于空数组,map返回的结果是空数组

  2. 对于非空数组,将第一个元素使用f进行映射,结果作为返回值(数组)的第一个元素,再对后面的剩余数组递归调用map f xs,作为返回值(数组)的剩余部分

直接将上面的代码“翻译”成JS的话,大概是这个样子

const map = f =>
    arr =>
        arr.length === 0 ?
            [] :
            [f(arr[0])].concat(map(f)(arr.slice(1)))

利用解构语法来简化的话大概是这个样子

const map = f =>
    ([x, ...xs]) =>
        x === undefined ?
            [] :
            [f(x), ...map(f)(xs)]

至于map的用法大家其实都是比较熟悉的了,这里就只做一个简单的例子

const double = arr =>
    map(x =>
        x * 2)(arr)
double([1, 2, 3, 4, 5]) 
// 结果是[2, 4, 6, 8, 10]


实现sum

接下来需要实现一个sum函数,对一个数组中的所有元素求和,有了map的递归思想,很容易写出来sum

const sum = accumulator =>
    ([x, ...xs]) =>
        x === undefined ?
            accumulator :
            sum (x + accumulator) (xs)
sum (0) ([1, 2, 3, 4, 5]) 
// 结果是15

依然会发现那个(0)传参是无比丑陋的,用一开始那个loop_on_array相同的思想提取一个函数

const _sum = accumulator =>
    ([x, ...xs]) =>
        x === undefined ?
            accumulator :
            _sum (x + accumulator) (xs)
const sum = xs =>
    _sum (0) (xs)

计划通。

实现reduce

比较map和sum可以发现事实上他们是非常相似的:

  1. 都是把数组拆解为(头,剩余)这个模式

  2. 都有一个“累加器”,在sum中体现为一个用来不断求和的数值,在map中体现为一个不断被扩充的数组

  3. 都通过“对头部执行操作,将结果与累加器进行结合”这样的模式来进行迭代

  4. 都以空数组为迭代的终点

也许你觉得上面的map实现并不是这个模式的,事实上它是的,不放把map按照这个模式重新实现一下

const _map = f =>
    accumulator =>
        ([x, ...xs]) =>
            x === undefined ?
                accumulator :
                _map (f) ([...accumulator, f(x)]) (xs)
const map = f =>
    xs =>
        _map (f) ([]) (xs)
map(x => x * 2)([1, 2, 3, 4, 5])

和sum的模式惊人的一致对么?这就是所谓的foldr,foldr是一个对这种迭代模式的抽象,我们把它简单的描述成:

// foldr :: (a -> b -> b) -> b -> [a] -> b
const foldr = f =>
    accumulator => ([x, ...xs]) =>
        x === undefined ?
            accumulator :
            f (x) (foldr(f)(accumulator)(xs))

其中f是一个“fold函数”,接收两个参数,第一个参数是“当前值”,第二个参数是“累加器”,f返回一个更新后的“累加器”。foldr会在数组上迭代,不断调用f以更新累加器,直到遇到空数组,迭代完成,则返回累加器的最后值。

下面我们用foldr来分别实现map和sum

const map = f =>
    foldr (x =>
        acc =>
            [f(x), ...acc]) ([])
const sum = foldr (x =>
    acc =>
        x + acc) (0)
map (x => x * 2) ([1, 2, 3, 4, 5]) 
// 结果是[2, 4, 6, 8, 10]
sum ([1, 2, 3, 4, 5]) 
// 结果是15

这时候你会发现foldr的定义其实就是JavaScript里自带的reduce函数,没错这俩定义是一样的,通过foldr或者说叫reduce抽象,我们实现了对数组的“有状态遍历”,相比于上面的loop_on_array则是“无状态遍历”,因为“累加器”作为状态,是在不断的被修改的(严格的说它不是被修改了,而是用一个新值取代了它)。

用foldr实现的sum非常形象,就像把摊成一列的扑克牌一张一张叠起来一样。

“有状态”当然可以实现“无状态”,不care状态不就行了吗,所以使用foldr来实现loop_on_array也是完全没问题的

const loop_on_array = f =>
    foldr(x =>
        _ =>
            f(x)) (undefined)
loop_on_array (x => console.log(x)) ([1, 2, 3, 4, 5])

呃,等等,为什么输出顺序是反的?是54321呢?很明显foldr中的r就表示它是“右折叠”,从递归的角度很好理解,无外乎先进后出嘛。所以要实现“左折叠”自然也有foldl函数(这里的左折叠右折叠表示折叠的起始方向,就跟东风北风一个道理):

// foldl :: (b -> a -> b) -> b -> [a] -> b
const foldl = f =>
    accumulator =>
        ([x, ...xs]) =>
            x === undefined ?
                accumulator :
                foldl (f) (f(accumulator)(x)) (xs)

用它重新实现loop_on_array,注意这次f的参数顺序和foldr是相反的,这次是accumulator在前、x在后,这样能更形象的表达“左折叠”的概念

const loop_on_array = f =>
     foldl(_ =>
         x =>
             f(x)) (undefined)
loop_on_array (x => console.log(x)) ([1, 2, 3, 4, 5])


循环小结

在第一个for循环的例子中,我们使用了命令式编程的思路,通过构造“顺序执行”组合函数来让“循环体”和“下一次迭代”这两个操作能够顺序执行。

这个思路毫无疑问是行得通的,但是似乎又有点命令式编程思想根深蒂固的感觉,于是在后面的例子里面,通过map、sum抽象出foldr和foldl函数,实现了“有状态遍历”。

foldr/foldl是对数组(列表)操作的一个高度抽象,它非常非常强大。

而在第一个例子实现for循环的过程中,我们费了老鼻子劲才构造出的“顺序执行”难道就这么被抛弃了?其实并没有,因为foldr/foldl抽象的是对列表的操作,而“顺序执行”则是更为普适的将两个操作的顺序进行安排的方式。

2. 实现局部变量

首先考虑这样一段代码

function getName() {
    return 'name from somewhere'
}
function greeting(word) {
    var name = getName()return word + ', ' + name
}
console.log(greeting('Hello'))

这是一段典型的命令式编程的代码,它最主要的问题就是局部变量name

在上一篇文章的第一个实现对数组遍历的例子当中我们已经对“顺序执行”初窥门径,通过构造了一个two_steps函数,实现两个步骤(函数)顺序执行。

在这个构造过程当中,我们得到一个重要的思路,就是在函数式语言当中,如果你想“获得”一个什么东西,就构造一个新的函数,把它参数化。对于two_steps的例子而言,“想获得”的是一个step2,就把它参数化了。

所以当需要“获得”局部变量的时候,自然而然我们会想到,把要拿的东西参数化就OK了,于是我们可以简单的这么构造:

// local :: a -> (a -> b) -> b
const local = a => f => f(a)

local函数接收两个参数,a是要“捕获”的值,f是接收或者说消费这个值的函数,用它来改造上面的代码

function greeting(word) {
    return local(getName())(name => word + ', ' + name)
}

上文当中将getName()的结果“捕获”作为后面函数的参数,实现了“局部变量”name。把上面的函数按照“真·函数式编程”规则改写:

const getName = () => 'name from somewhere';
const greeting = word => local(getName())(name => word + ', ' + name);
console.log(greeting('Hello')); 
// 结果是'Hello, name from somewhere'

不难发现我们这个local其实就是two_steps的简化版,区别在于two_steps的第一个step1是一个函数,而local则是一个值,如果用two_steps实现local那么就是:

const local = value =>
    step2 =>
        two_steps (_ 
            => value) (step2) ()

看,我们这个local的风格,看起来非常像JS当中的“回调”的方式——事实上,因为像Haskell这样的纯函数式语言没有顺序执行,你可以认为每一行代码执行顺序是不一定的,这非常类似于在JS中我们遇到了海量异步操作的时候:异步操作的执行顺序是不一定的,所以才会用回调函数来保证“异步操作->处理结果”这个顺序。回调是一种非常朴素,非常好理解,但写起来却反人类的异步编程方式。我一直不批判浏览器和node.js里把API都用回调风格来定义,因为它很原始,大家都懂,至于后来的如Promise这些方式,也可以用回调的API轻松封装出来,咸甜酸辣,五仁叉烧,任君挑选。

OK,扯远了,也许你觉得上面的例子太过简单,下面我们来看这篇文章中真正重点的内容。

3. 实现状态

以下的例子基本上都源自从陈年译稿——一个面向Scheme程序员的monad介绍文中搬运和改造,我从这篇文章获得了巨大的启发,也先对作者和译者表示感谢。

我们写程序的过程当中常常回用到自增ID这种东西,如果在JS里要实现一个自增ID,可能会这么写

var __guid = 0
function guid() {
    return __guid++
}

好嘛,绕了一圈,又回到刚才的话题了,局部变量(这次在闭包里面而已,本质是一样的),和二次赋值。但是经过前文的启发很容易就能用参数化的方式来解决这个问题

function guid(oldValue) {
    var newValue = oldValue + 1return [newValue, newValue]
}

也就是

const guid = oldValue =>
    local(oldValue + 1)(newValue =>
        [newValue, newValue])

我们把局部变量参数化,然后把返回值改成了一个数组(因为JS里没有元组,所以只能用数组暂代),这样在需要guid的时候,需要把之前的返回值的第二个值作为参数传进去;而整个程序则需要使用一个初始值(我们管叫“种子”)来启动它。

现在假如我们有3个名字,分别要对它们用guid来编号并且输出,也就是说需要连续执行3次guid,这里涉及到的就是顺序执行以及guid的状态参数传递:

const counting = state =>
    local (guid(state)) (([id1, state1]) =>
        local (guid(state1)) (([id2, state2]) =>
            local (guid(state2)) (([id3, state3]) =>
                `Alice:${id1} Bob:${id2} Chris:${id3}`)));
console.log(counting(0));
// 结果是Alice 0, Bob 1, Chris 2

也许你已经被后面谜一般的这一堆括号所惹毛了,如果你能忍着继续看下去的话也许可以真的获得这篇文章的乐趣(捂脸

对于没有副作用的函数(纯函数),不需要带上state这个参数,而对于有副作用的函数——我们称之为“操作”——这里体现为调用了guid函数的,就需要带上state这个参数。

看,state就是我们所说的“状态”,整个过程中,都把它(用数组的第二个值)揣着,当一个函数需要状态的时候就传给它,它用完了又捡回来揣着。

看起来没什么不对,但是guid(state)这个函数总是给人隐隐的不爽:state是guid自身的状态,却需要counting这个消费者在整个调用过程当中帮它传递,这是不爽的,因此不妨把guid的state参数定义为curry形式:

const guid = () => 
    state =>
        local(state + 1)(state1 => 
            [state1, state1])

进而counting中的local的第一步不再是一个已算出的值,而是一个curry了第一个参数(空),需要第二个参数state的guid函数,这就是curry函数的精妙之处,它让函数可以“部分地”被执行,从而能够实现演算——我们把整个演算过程像代数推导一样列出来,最后把值代入就能计算出结果,是不是很像中学的时候解代数题、物理题什么的?

于是,用了新的guid以后,local就不能应用于guid了,使用two_steps改写一下

const counting_by_steps = state =>
    two_steps(guid()) (([id1, state1]) =>
        two_steps(guid()) (([id2, state2]) =>
            two_steps(guid()) (([id3, state3]) =>
                `Alice:${id1} Bob:${id2} Chris:${id3}`)(state2))(state1))(state);
console.log(counting_by_steps(0));

这时候你会觉得更烦了,因为这次虽然我们不需要给guid()主动传递state了,但在连续多次调用two_steps的时候,却需要把state1和state2给two_steps传递下去,能不能构造一个新的two_steps函数,让它能够透明地传递state参数呢?

答案是显然的,回顾一下上文中two_steps的定义和实现:

// two_steps :: (a -> b) -> (b -> c) -> a -> c
const two_steps = step1 => step2 =>
    param =>step2(step1(param))

我们想想,two_steps的param参数作用无外乎是作为“状态”传给step1,它的定义是curry化的,如果two_steps不传第三个参数,获得的就是一个内容为step1-then-step2的“部分函数”这个函数接收param参数,返回step2的结果。要让two_steps能够继续透明地使用这种“部分函数”,就是说two_steps的结果可以继续被two_steps组合,我们可以对step1和step2函数的类型进行限定

step1 :: State -> [Value, State]
step2 :: State -> [Value, State]
param :: State

其中State是状态的类型,Value是返回值的类型,在guid的例子里面,这两者都是Number。这样结合起来,新的two_steps——我们给它一个新名字叫begin——的类型限定就是

begin :: (State -> [Value, State]) -> (State -> [Value, State]) -> State -> [Value, State]

对吧,begin的两个参数step1和step2都是State -> [Value, State]类型,这跟它在只curry前两个参数,剩余param参数时的那个部分函数step3的类型(函数签名)是一样 的。

从中抽取出一个模式:State -> [Value, State],我们用一个泛型来抽象它叫做M<Value>,不难发现,guid是() -> Number -> [Number, Number]也就是() -> M<Number>类型(其State也是Number类型),用这个泛型可以把begin描述成:

begin :: M<Value> -> M<Value> -> State -> [Value, State]

这样我们可以顺利的推出begin的实现

const begin = step1 =>
    step2 =>
        state =>
            {const [value1, state1] = step1(state);
            return step2(state1)
}

简化之,结果是

const begin = step1 =>
    step2 =>
        state =>
            step2(step1(state)[1])

这和two_steps如出一辙,区别只在于,对于step2,它丢弃了step1所产生的Value,而只保留了它所产生的State

然而我们还是需要Value啊!说丢就丢这也太不负责任了吧!这时候自然想到“再加一个中间层”,我们设计这样一个函数:它的第二个参数消费step1所产生的Value,返回step2,这个step2再去消费step1所产生的State。把这个函数命名为bind,它的类型描述如下

bind :: (State -> [Value, State]) -> (Value -> (State -> [Value, State])) -> State -> [Value, State]

使用M泛型来描述它就是

bind :: M<Value> -> (Value -> M<Value>) -> State -> [Value, State]

看,当使用bind来结合两个操作step1和build_step2的时候,step1消费掉State种子,产生一个返回值Value(并且可能产生了新的状态State)。紧接着build_step2消费了step1所返回的Value,并且它返回一个新的M<Value>也就是step2,bind函数会像begin那样,把step1所产生的新State作为参数传给step2,并且返回其结果。于是我们终于构造出了bind的实现:

const bind = step1 =>
    build_step2 =>
        state => 
            {const [value1, state1] = step1(state);
            const step2 = build_step2(value1);
            return step2(state1)
}

转换成“真·函数式编程”,这里利用local实现局部变量:

const bind = step1 =>
    build_step2 =>
        state =>
            local (step1(state)) (([value1, state1]) =>
                local (build_step2(value1)) (step2 =>
                    step2(state1)))

不难发现,beginbind的一个特例,用bind来构造它的话就是

const begin = step1 => step2 => bind (step1) (_ => step2)

非常的直白,忽略step1产生的Value,继续调用step2。现在使用bind来改进上面的多个带状态的顺序执行的代码

const returns = value =>
    state =>
        [value, state]
const main = state =>
    bind (guid()) (id1 =>
        bind (guid()) (id2 =>
            bind (guid()) (id3 =>
                returns(`Alice:${id1} Bob:${id2} Chris:${id3}`)))) (state) [0]
console.log(main(0)) 
// 结果是'Alice:1 Bob:2 Chris:3'

注意上面的代码里面我们定义了一个returns函数,它接收一个Value,并且返回一个M<Value>。毫无疑问,M<Value>是比Value更“高阶”的类型,因为M<Value>当中含有State,而Value不含。

bind函数作用于M<Value>类型,因此需要returns,于是通过returns将一个Value转成M<Value>;而main函数是返回Value类型的,则通过[0]来将一个M<Value>抛弃State转回Value。

还记得刚才那句话吗:

对于没有副作用的函数(纯函数),不需要带上state这个参数,而对于有副作用的函数——我们称之为“操作”——这里体现为调用了guid函数的,就需要带上state这个参数。

因为main函数抛弃了最终的State,所以它不在有副作用,又变成纯函数了;而正是因为它抛弃了State,它自身也变成无状态的了,所以毫无疑问重新调用main(0)就会让guid清零重新开始——局部变量,作用域,生命周期,有没有发现命令式编程里面的概念在这里体现出来了?

那么,M<Value>到底是什么?

从面相对象的角度去理解,我们可以说,M<Value>是一个封装了State在里面的“操作”。从函数式编程的角度去理解,我们理解为Value是一个“值”,而M<Value>是一个“计算”。

对上面的东西做一个小结

  1. returns函数将一个Value“提升”成M<Value>类型。

  2. begin和bind函数将两个M<Value>绑定顺序关系(begin是bind的简化版)。bind函数中的build_step2将会有一个“临时”的机会获得Value,但是用完以后又必须回到M<Value>。

  3. [0]将M<Value>“降级”回Value——这个过程将会不可逆地丢失掉State。

我们上面的returns和bind这一对函数就实现了一个Monad——准确的说是State Monad。在Haskell里面,我们的returns叫做return,我们的bind叫做bind或者运算符>>=。这张图

via《道可叨 | Monad 最简介绍》via《道可叨 | Monad 最简介绍》

是我所见到的一个非常形象的描述。

除了State Monad外还有很多种Monad,例如Maybe帮助Haskell实现了非常自然的错误处理,IO帮助Haskell实现了IO。在Monad的帮助之下Haskell更实现了do notation这种“顺序执行”的语法糖,可谓是Haskell的核心之一。

4. 总结

到现在为止,循环有了,局部变量有了,状态也有了,可以说基本上已经没有写不出的程序了。当然,正经的程序是不可能这么写的,所以这两篇文章也就是分享一下我个人的学习心得,玩玩所谓“真·函数式编程”,以及——最重要的——还是装逼。


登录后方可回帖

登 录
信息栏

Scheme中文社区

推荐实现 ChezScheme / r6rs / r7rs large
theschemer.org
Q群: 724577239

精华导览

社区项目

包管理器:Raven
HTTP服务器:Igropyr (希腊火)
官方插件:vscode-chez

社区目标:

完善足以使Scheme工程化和商业化的库,特别是开发极致速度的Web服务器和ANN模块。

一直以来Scheme缺少一个活跃的中文社区,同时中文资料的稀少,导致大多数因为黑客与画家和SICP而接触Scheme的朋友,在学完SICP后无事可做,不能将Scheme转换为实际的生产力。最后渐渐的放弃。
同时Chicken等实现,却因效率问题无法与其他语言竞争。本社区只有一个目的,传播Scheme的文明之火,在最快的编译器实现上,集众人之力发展出足够与其他语言竞争的社区和库。


友情链接:

Clojure 中文论坛
函数式·China


Loading...