函数式编程指北

发布于 作者: Ethan

引言

本文是函数式编程指北的快速记录

一等公民函数

WHY:

  1. 消除冗余代码/减少修改成本 e.g.

    // before
    httpGet('/post/2', json => renderPost(json));
    
    // 如果要修改,胶水函数也需修改
    httpGet('/post/2', (json, err) => renderPost(json, err));
    
    // after
    httpGet('/post/2', renderPost);
    
  2. 避免特定命名,增加复用性

    // 只针对当前的博客
    const validArticles = articles =>
    articles.filter(article => article !== null && article !== undefined),
    
    // 对未来的项目更友好
    const compact = xs => xs.filter(x => x !== null && x !== undefined);
    

警惕this陷阱

var fs = require('fs');

// 太可怕了
fs.readFile('freaky_friday.txt', Db.save);

// 好一点点
fs.readFile('freaky_friday.txt', Db.save.bind(Db));

函数式编程本身会尽量避免使用this(因其依赖上下文,破坏函数纯性),仅在对接面向对象类库时需兼容处理。

纯函数

纯函数定义:

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

副作用定义:

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

好处:减少外部依赖,减少认知负荷。 不是要禁止使用一切副作用,而是说,要让它们在可控的范围内发生。

纯函数好处

  1. 可缓存性(Cacheable)

    简单来说,就是memoize,类似python的@cache

    e.g.

    var squareNumber  = memoize(function(x){ return x*x; });
    
    squareNumber(4);
    //=> 16
    
    squareNumber(4); // 从缓存中读取输入值为 4 的结果
    //=> 16
    

    小技巧:通过延迟执行将不纯函数转换为纯函数:

    var pureHttpCall = memoize(function(url, params){
    return function() { return $.getJSON(url, params); }
    });
    

    之所以纯是因为它总是会根据相同的输入返回相同的输出:给定了 url 和 params 之后,它就只会返回同一个发送 http 请求的函数。

  2. 可移植性/自文档化(Portable / Self-Documenting)

    自给自足,所以依赖明确,易于观察&理解。从函数签名即可理解足够多信息。

    e.g.

    var signUp = function(Db, Email, attrs) {
    
    return function() {
        var user = saveUser(Db, attrs);
        welcomeUser(Email, user);
    };
    };
    

    其次,强迫依赖注入使得应用更灵活。函数甚至可以序列化通过socket发送。

    我最喜欢的名言之一是 Erlang 语言的作者 Joe Armstrong 说的这句话:“面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩...以及整个丛林”。

  3. 可测试性(Testable) 很容易明白。

  4. 合理性(Reasonable)

    引用透明性(referential transparency):如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。

    纯函数保证了引用透明性。

    技术:等式推导(Equational Reasoning):

    var punch = function(player, target) {
        if(player.team === target.team) {
            return target;
        } else {
            return decrementHP(target);
        }
    };
    

    由于数据不可变,将team替换为实际值:

    var punch = function(player, target) {
        if("red" === "green") {
            return target;
        } else {
            return decrementHP(target);
        }
    };
    

    if语句结果为false,可以删除:

    var punch = function(player, target) {
        return decrementHP(target);
    };
    

    简化:

    var punch = function(player, target) {
        return target.set("hp", target.hp-1);
    };
    
  5. 并行(最重要) 可以并行运行任意纯函数,因为不需要访问共享内存,无副作用,自然没有竞态情况。

柯里化(Curry)

柯里化:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

简单的例子:

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

更好些的例子:

var curry = require('lodash').curry;

var match = curry(function(what, str) {
  return str.match(what);
});

var replace = curry(function(what, replacement, str) {
  return str.replace(what, replacement);
});

var filter = curry(function(f, ary) {
  return ary.filter(f);
});

var map = curry(function(f, ary) {
  return ary.map(f);
});

只需传给函数一些参数,就能得到一个新函数。

e.g.

var getChildren = function(x) {
  return x.childNodes;
};

var allTheChildren = map(getChildren);

// v.s.

var allTheChildren = function(elements) {
  return _.map(elements, getChildren);
};

哪怕输出是另一个函数,它也是纯函数。

代码组合

函数饲养

组合:

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

将两个函数结合产生新函数。

e.g.

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);

shout("send in the clowns");

这样很容易看出是从右向左的数据流,可读性大于嵌套函数。 组合满足结合律:

var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);

因为满足结合律,所以调用分组不重要,所以可以让参数可变:

var lastUpper = compose(toUpperCase, head, reverse);

Pointfree

Pointfree style means never having to say your data 函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。

e.g.


// example 1
var snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};

// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

// example 2
var initials = function (name) {
  return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};

// pointfree
var initials = compose(join('. '), map(compose(toUpperCase, head)), split(' '));

initials("hunter stockton thompson"); 
// H.S.T

好处:减少不必要命名,保持简洁&通用,但需要注意不是所有的函数式代码都是Pointfree的。

常见错误

// 错误做法:我们传给了 `angry` 一个数组,根本不知道最后传给 `map` 的是什么东西。
var latin = compose(map, angry, reverse);

latin(["frog", "eyes"]);
// error


// 正确做法:每个函数都接受一个实际参数。
var latin = compose(map(angry), reverse);

latin(["frog", "eyes"]);
// ["EYES!", "FROG!"])

Debug

可以用以下不纯函数来debug:

var trace = curry(function(tag, x){
  console.log(tag, x);
  return x;
});

var dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' '));

dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined

// tracing:
var dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/\s{2,}/ig, ' '));
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]

// fix:
var dasherize = compose(join('-'), map(toLower), split(' '), replace(/\s{2,}/ig, ' '));

dasherize('The world is a vampire');

// 'the-world-is-a-vampire'

范畴学

对象的搜集 对象就是数据类型,例如 String、Boolean、Number 和 Object 等等。通常我们把数据类型视作所有可能的值的一个集合(set)。像 Boolean 就可以看作是 [true, false] 的集合,Number 可以是所有实数的一个集合。把类型当作集合对待是有好处的,因为我们可以利用集合论(set theory)处理类型。

态射的搜集 态射是标准的、普通的纯函数。

态射的组合 就是本章介绍的组合。Compose 函数是符合结合律的,这并非巧合,结合律是在范畴学中对任何组合都适用的一个特性。

compose illustration 上图展示了什么是组合

组合像一系列管道那样把不同的函数联系在一起,数据就可以也必须在其中流动——毕竟纯函数就是输入对输出,所以打破这个链条就是不尊重输出,就会让我们的应用一无是处。 我们认为组合是高于其他所有原则的设计原则,这是因为组合让我们的代码简单而富有可读性。另外范畴学将在应用架构、模拟副作用和保证正确性方面扮演重要角色。

示例程序

// 注释为llm添加

// 配置RequireJS,设置模块别名和路径
// requirejs.config():RequireJS的配置方法,用于定义模块加载的路径和其他选项
requirejs.config({
  // paths属性:定义模块名称到模块文件路径的映射
  paths: {
    // 将"ramda"模块映射到指定的CDN路径,加载Ramda函数式库
    ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
    // 将"jquery"模块映射到指定的CDN路径,加载jQuery库
    jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
  }
});

// 加载所需模块,使用RequireJS的异步加载机制
// require():RequireJS的模块加载方法,第一个参数是依赖数组,第二个是回调函数
require([
    'ramda',   // 加载Ramda函数式编程库
    'jquery'   // 加载jQuery库
  ],
  // 模块加载完成后执行的回调函数,参数对应依赖数组中的模块
  // _:Ramda库的实例,$:jQuery库的实例
  function (_, $) {
    ////////////////////////////////////////////
    // 工具函数和不纯函数定义(会产生副作用的函数)

    // 定义Impure对象,包含所有会产生副作用的函数
    var Impure = {
      // 封装$.getJSON为柯里化函数,用于获取JSON数据
      // _.curry():Ramda的柯里化函数,将多参数函数转换为可分步调用的函数
      // callback:数据获取成功后的回调函数
      // url:请求的URL地址
      getJSON: _.curry(function(callback, url) {
        // $.getJSON():jQuery的AJAX方法,用于从服务器获取JSON数据
        $.getJSON(url, callback);
      }),

      // 封装设置HTML内容的操作,柯里化函数
      // sel:DOM选择器
      // html:要设置的HTML内容
      setHtml: _.curry(function(sel, html) {
        // $(sel).html(html):jQuery方法,设置匹配元素的HTML内容
        $(sel).html(html);
      })
    };

    // 创建图片元素的函数
    // url:图片的src属性值
    var img = function (url) {
      // $('<img />', { src: url }):jQuery创建图片元素的方法,设置src属性
      return $('<img />', { src: url });
    };

    // 调试用的跟踪函数,柯里化函数,用于在函数组合中输出中间结果
    // tag:标识信息,x:要跟踪的值
    var trace = _.curry(function(tag, x) {
      // console.log():浏览器控制台输出
      console.log(tag, x);
      // 返回输入值,保证函数组合的连续性
      return x;
    });

    ////////////////////////////////////////////
    // 业务逻辑函数(纯函数,无副作用)

    // 生成Flickr API请求URL的函数
    // t:搜索标签
    var url = function (t) {
      // 拼接URL字符串,返回完整的API请求地址
      return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + t + '&format=json&jsoncallback=?';
    };

    // 从Flickr数据项中提取图片URL的函数
    // 使用Ramda的函数组合:先获取'media'属性,再获取其'm'属性
    // _.compose(f, g):Ramda的函数组合,返回一个新函数,等价于f(g(x))
    // _.prop('media'):获取对象的'media'属性值
    // _.prop('m'):获取对象的'm'属性值
    var mediaUrl = _.compose(_.prop('m'), _.prop('media'));

    // 从API响应中提取所有图片URL的函数
    // 先获取'items'属性,再对每个项应用mediaUrl函数
    // _.map(mediaUrl):对数组中的每个元素应用mediaUrl函数
    // _.prop('items'):获取响应对象的'items'属性(图片数组)
    var srcs = _.compose(_.map(mediaUrl), _.prop('items'));

    // 将图片URL数组转换为图片元素数组的函数
    // 先获取图片URL数组,再将每个URL转换为img元素
    // _.map(img):对每个URL应用img函数,生成图片元素
    var images = _.compose(_.map(img), srcs);

    // 渲染图片到页面的函数
    // 先将图片URL转换为图片元素,再将这些元素设置为body的HTML内容
    // Impure.setHtml("body"):设置body元素的HTML内容
    var renderImages = _.compose(Impure.setHtml("body"), images);

    // 应用的主函数,组合URL生成和数据获取逻辑
    // 先根据标签生成URL,再使用该URL获取数据并渲染图片
    // Impure.getJSON(renderImages):获取JSON数据后调用renderImages渲染
    var app = _.compose(Impure.getJSON(renderImages), url);

    // 启动应用,搜索标签为"cats"的图片
    app("cats");
  });

Hindley-Milner类型签名

e.g.

//  match :: Regex -> String -> [String]
var match = curry(function(reg, s){
  return s.match(reg);
});

简单来说,每传一个参数,就会弹出签名最前面的那个类型,也可以进行分组:

// match :: Regex -> (String -> [String])

更多示例:

//  id :: a -> a
var id = function(x){ return x; }

//  map :: (a -> b) -> [a] -> [b]
// map 接受两个参数,第一个是从任意类型 a 到任意类型 b 的函数;第二个是一个数组,元素是任意类型的 a;map 最后返回的是一个类型 b 的数组
var map = curry(function(f, xs){
  return xs.map(f);
});

// 更复杂一些的:
//  reduce :: (b -> a -> b) -> b -> [a] -> b
var reduce = curry(function(f, x, xs){
  return xs.reduce(f, x);
});

缩小可能性范围

引入类型变量会出现的特性(Parametricity)。 比如说:

// head :: [a] -> a

通过这个类型签名,我们可以猜测这个方法与类型无关,a可以是任意类型,且判断这个函数不能对a做任何特定的事情。

自由定理

也是引入类型变量带来的。

// head :: [a] -> a
compose(f, head) == compose(head, map(f));

// filter :: (a -> Bool) -> [a] -> [a]
compose(map(f), filter(compose(p, f))) == compose(filter(p), map(f));

第一个例子中,等式左边说的是,先获取数组的头部,然后对它调用函数 f; 等式右边说的是,先对数组中的每一个元素调用 f,然后再取其返回结果的头部。 这两个表达式的作用是相等的,但是前者要快得多。

第二个例子 filter 也是一样。等式左边是说,先组合 f 和 p 检查哪些元素要过滤掉,然后再通过 map 实际调用 f; 等式右边是说,先用 map 调用 f,然后再根据 p 过滤元素。这两者也是相等的。

类型约束

签名可以把类型约束为一个特定的接口。

// sort :: Ord a => [a] -> [a]

该签名表示a一定是个Ord对象,i.e. a必须实现Ord接口。 这样我们可以获取a,函数的更多信息,并限定函数的作用范围。 我们把这种接口声明称为类型约束(Type Constraints)

容器

目前已知信息:

  1. 函数式程序使用管道将数据在纯函数间传递。
  2. 程序是声明式的。

接下来应当考虑:

  1. Control Flow
  2. Error Handling
  3. Async Actions
  4. State
  5. Effects

创建容器

容器可以装任何类型的值,是对象,但是不存在OOP下的属性/方法:

var Container = function(x) {
  this.__value = x;
}

// 使用of作为ctor,暂时认定为是将值放入容器的方式
Container.of = function(x) { return new Container(x); };

关于Container

  1. Container 是个只有一个属性的对象。尽管容器可以有不止一个的属性,但大多数容器还是只有一个。我们很随意地把 Container 的这个属性命名为 __value。
  2. __value 不能是某个特定的类型,不然 Container 就对不起它这个名字了。
  3. 数据一旦存放到 Container,就会一直待在那儿。我们可以用 .__value 获取到数据,但这样做有悖初衷。

Functor

有了值,自然也需要操控的方式。

// (a -> b) -> Container a -> Container b
Container.prototype.map = function(f){
  return Container.of(f(this.__value))
}

// 使用:
Container.of(2).map(function(two){ return two + 2 })
//=> Container(4)

Functor: 是实现了 map 函数并遵守一些特定规则的容器类型。

map使我们能够不离开Container的情况下操作里面的值,也可以连续调用。 当 map 一个函数的时候,我们请求容器来运行这个函数。

e.g. Maybe

// Maybe 最常用在那些可能会无法成功返回结果的函数中
// 这样使用map时就可以避免空值了
var Maybe = function(x) {
  this.__value = x;
}

Maybe.of = function(x) {
  return new Maybe(x);
}

Maybe.prototype.isNothing = function() {
  return (this.__value === null || this.__value === undefined);
}

Maybe.prototype.map = function(f) {
  return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}

// pointfree风格
//  map :: Functor f => (a -> b) -> f a -> f b
var map = curry(function(f, any_functor_at_all) {
  return any_functor_at_all.map(f);
});

// usecase
//  safeHead :: [a] -> Maybe(a)
var safeHead = function(xs) {
  return Maybe.of(xs[0]);
};

var streetName = compose(map(_.prop('street')), safeHead, _.prop('addresses'));

streetName({addresses: []});
// Maybe(null)

streetName({addresses: [{street: "Shady Ln.", number: 4201}]});
// Maybe("Shady Ln.")

// 有时可以明确返回一个null来表示失败:
var withdraw = curry(function(amount, account) {
  return account.balance >= amount ?
    Maybe.of({balance: account.balance - amount}) :
    Maybe.of(null);
});

释放容器中的值

我们的代码,就像薛定谔的猫一样,在某个特定的时间点有两种状态,而且应该保持这种状况不变直到最后一个函数为止。

// 可以使用帮助函数
//  maybe :: b -> (a -> b) -> Maybe a -> b
var maybe = curry(function(x, f, m) {
  return m.isNothing() ? x : f(m.__value);
});

//  getTwenty :: Account -> String
var getTwenty = compose(
  maybe("You're broke!", finishTransaction), withdraw(20)
);


getTwenty({ balance: 200.00});
// "Your balance is $180.00"

getTwenty({ balance: 10.00});
// "You're broke!"

一些语言中,Maybe被伪装成Optional

“纯”错误处理

使用Either类型。 传统try/catch不纯,会中断流程。 Either通过返回值明确成功或失败,且携带相关信息。

Either包含两个子类:LeftRight,都是Functor:

var Left = function(x) {
  this.__value = x;
}

Left.of = function(x) {
  return new Left(x);
}

Left.prototype.map = function(f) {
  return this;
}

var Right = function(x) {
  this.__value = x;
}

Right.of = function(x) {
  return new Right(x);
}

Right.prototype.map = function(f) {
  return Right.of(f(this.__value));
}

Left:表示错误 / 失败情况,内部存储错误信息。 map方法:忽略传入的函数,直接返回自身(“短路” 特性)。 构造:Left.of(x)(x 为错误信息)。`

Right:表示成功情况,内部存储正确结果。 map方法:应用传入的函数处理内部值,返回新的 Right(与 Identity 函子类似)。 构造:Right.of(x)(x 为正确结果)。

// 成功时:Right会处理值
Right.of("rain").map(str => "b" + str); // Right("brain")

// 失败时:Left忽略处理,保留错误信息
Left.of("rain").map(str => "b" + str); // Left("rain")

实际应用:

// getAge :: Date -> User -> Either(String, Number)
const getAge = curry((now, user) => {
  const birthdate = moment(user.birthdate, 'YYYY-MM-DD');
  if (!birthdate.isValid()) return Left.of("生日解析失败");
  return Right.of(now.diff(birthdate, 'years'));
});

// 成功情况
getAge(moment(), {birthdate: '2005-12-12'}); // Right(18)
// 失败情况
getAge(moment(), {birthdate: 'invalid'}); // Left("生日解析失败")

统一处理:either 方法 either函数接受两个处理函数(分别处理 LeftRight),强制覆盖两种情况,返回统一类型结果:

// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = curry((leftHandler, rightHandler, e) => {
  switch(e.constructor) {
    case Left: return leftHandler(e.__value);
    case Right: return rightHandler(e.__value);
  }
});

// 示例:统一打印结果或错误
const zoltar = compose(console.log, either(id, fortune), getAge(moment()));
zoltar({birthdate: '2005-12-12'}); // 打印"若存活,你将年满 19"
zoltar({birthdate: 'invalid'}); // 打印"生日解析失败"

Effects

IO:捕获&延迟非纯操作,同时支持链式处理。

var IO = function(f) {
  this.__value = f;
}

IO.of = function(x) {
  return new IO(function() {
    return x;
  });
}

IO.prototype.map = function(f) {
  return new IO(_.compose(f, this.__value));
}

// example
// 1. 捕获“获取window”的非纯操作,生成IO
var io_window = new IO(() => window);

// 2. 链式map:仅组合函数,不执行
var io_width = io_window.map(win => win.innerWidth); // IO(函数:获取innerWidth)
var io_href = io_window.map(_.prop('location')).map(_.prop('href')); // IO(函数:获取href)

// 3. 最终触发:调用unsafePerformIO()执行所有延迟操作
console.log(io_width.unsafePerformIO()); // 实际输出宽度(如1430)

代码虽积累不纯操作,但只有最终调用是不纯的:


////// 纯代码库: lib/params.js ///////

//  url :: IO String
var url = new IO(function() { return window.location.href; });

//  toPairs =  String -> [[String]]
var toPairs = compose(map(split('=')), split('&'));

//  params :: String -> [[String]]
var params = compose(toPairs, last, split('?'));

//  findParam :: String -> IO Maybe [String]
var findParam = function(key) {
  return map(compose(Maybe.of, filter(compose(eq(key), head)), params), url);
};

////// 非纯调用代码: main.js ///////

// 调用 __value() 来运行它!
findParam("searchTerm").__value();
// Maybe(['searchTerm', 'wafflehouse'])

Async

Task:以纯函数形式封装异步操作。支持链式处理(类似同步代码),避免回调嵌套,同时内置错误处理。

核心部分 实现逻辑
构造函数 new Task((reject, resolve) => { ... })
- reject:异步失败时调用(传递错误);
- resolve:异步成功时调用(传递结果)。
Task.of(x) 生成包含“同步固定值x”的Task,支持统一的链式处理(如Task.of(3).map(n => n+1))。
map(f) 不立即执行f,而是将其注册为“异步结果的处理函数”,返回新Task(类似Promise的then)。
执行触发 需调用fork(rejectHandler, resolveHandler)
- 触发异步操作;
- 非阻塞(不阻塞主线程/Event Loop);
- 分别处理失败(rejectHandler)和成功(resolveHandler)。

实际应用示例

Task可封装各类异步操作(文件读取、HTTP请求等),并通过map链式处理结果。

示例1:Node.js读取文件

var fs = require('fs');

// 1. 封装异步读文件:readFile :: String -> Task(Error, String)
var readFile = function(filename) {
  return new Task((reject, resolve) => {
    fs.readFile(filename, 'utf-8', (err, data) => {
      err ? reject(err) : resolve(data); // 失败reject,成功resolve
    });
  });
};

// 2. 链式处理:读文件 → 按行分割 → 取第一行(仅注册操作,不执行)
var firstLineTask = readFile("metamorphosis")
  .map(split('\n')) 
  .map(head);

// 3. 触发执行:fork处理结果/错误
firstLineTask.fork(
  err => console.error("读文件失败:", err),
  line => console.log("第一行:", line) // 输出小说首句
);

示例2:jQuery异步请求JSON

// 1. 封装GET请求:getJSON :: String -> {} -> Task(Error, JSON)
var getJSON = curry((url, params) => {
  return new Task((reject, resolve) => {
    $.getJSON(url, params, resolve).fail(reject); // 成功resolve,失败reject
  });
});

// 2. 链式处理:请求视频数据 → 取标题
var videoTitleTask = getJSON('/video', {id: 10})
  .map(_.prop('title'));

// 3. 触发执行
videoTitleTask.fork(
  err => $("#error").html(err.message),
  title => $("#video-title").html(title) // 输出“Family Matters ep 15”
);

Task与其他函子的关联

  • 与IO的相似性:均为“延迟执行”的容器,不主动触发操作(IO需unsafePerformIO,Task需fork);IO可视为Task的特殊情况(同步非纯操作)。

  • 与Either的融合:Task内置“失败/成功”双分支,天然包含Either的错误处理能力(reject对应Left,resolve对应Right),无需额外封装。

  • 组合使用示例:异步读配置文件(Task)→ 验证配置合法性(Either)→ 连接数据库(IO):

    // 1. 读配置文件(Task)→ 解析JSON → 验证配置(Either)→ 连接数据库(IO)
    var getConfig = compose(
      map(compose(connectDb, JSON.parse)), // connectDb返回Either(IO(连接))
      readFile // 读文件返回Task(Error, String)
    );
    
    // 2. 触发执行:处理Task失败 + Either分支
    getConfig("db.json").fork(
      err => logErr("读文件失败:", err), // Task失败
      either( // Task成功后,处理Either的Left/Right
        err => console.log("配置错误:", err), // Either Left
        ioConn => ioConn.unsafePerformIO() // Either Right:执行IO连接数据库
      )
    );
    

核心优势与注意事项

  • 优势
    1. 控制流线性化:异步代码可按“同步链式”编写,避免回调嵌套,可读性高。
    2. 纯不纯分离:核心逻辑(map中的处理函数)保持纯,仅fork触发非纯操作。
    3. 非阻塞执行:fork不阻塞主线程,符合异步编程本质(如fork后可立即显示加载中动画)。
  • 注意事项
    1. 必须调用fork:未调用fork的Task仅注册操作,不会实际执行。
    2. 错误处理明确:需在fork中显式处理reject分支,避免遗漏错误。