我在函数式编程上犯下的几个错误
【CSDN 编者按】提到编程思想,你首先想到的会是面向对象还是面向函数编程呢?本文作者分享了自己在函数式编程实践中踩过的一些坑,分享给大家,希望能对你有所帮助。
原文链接:https://robertwpearce.com/how-to-lose-functional-programming-at-work.html
未经授权,禁止转载!
作者 | Robert Pearce 译者 | 弯月
出品 | CSDN(ID:CSDNnews)
在过去几年里,我在使用函数式编程开发拥有大量 JavaScript 代码的应用程序时,犯了不少错误,也走了一些弯路,再此复盘并分享给大家。
不使用静态类型检查
不使用如下工具:
-
TypeScript
-
Flow
-
ReasonML
-
Elm
const processData = composeP(syncWithBackend, cleansePII, validateData)
// * What arguments and their types are expected here?
//
// * If each function is written like this, how can
// one suss out what data are flowing where?
//
// * How hard is this going to be to debug?
// Use this everywhere: `(x) => (console.log(x), x)`
这些参数的类型应该是什么?如果每个函数都这样写,谁能猜出它们传递的是什么数据?这段代码的调试会非常困难。
或许你认为,这种““Point-free or die””的编程风格才是真正的问题。那么来看看这两种写法:
async function processData(data) {
await validateData(data)
const cleansedData = cleansePII(data)
await syncWithBackend(cleansedData)
return data
}
或者使用Promise链:
// or for the Promise-chainers…
const processData = data =>
validateData(data)
.then(cleansePII)
.then(syncWithBackend)
.then(() => data)
无论哪种写法,想想三个月之后还有谁能明白它们是做什么的。
不使用众所周知的代码文档工具
比如 jsdoc。这些工具会加剧其他团队成员理解代码的难度,同时还无法使用自动补齐。
下面是一个例子,注意其中的文档标签并不是jsdoc的标准标签:
// NOTE: this is an untested, small example
/**
* @typedef {Object} ReportingInfo
* @property {("light"|"dark")} userTheme - Current user's preferred theme
* @property {string} userName - Current user's name
* @property {UUID} postId - The current post's ID
*/
/**
* Validates that the reporting data (current user site prefences and post info)
* is OK, removes personally identifiable information, syncs this info with the
* backend, and gives us back the original data.
*
* @param {ReportingInfo} data - The current user's site preferences and post info
* @returns {Promise<ReportingInfo>} - The original reporting data
*/
const processData = data => // …
不认真培训新老同事
不要以为写几篇文章、找一些学习资源,交给刚接触函数式编程的人,鼓励他们问问题,就能有好结果。
也不要走向另一个极端:把所有的时间精力都花在几个人身上,忽略其他人,不记录学习经验,还不鼓励他们互相帮助。
不必麻烦其它工程团队加入,也无需所有人朝同一个方向努力
“我构建出来,他们就会注意到……不是吗?”
利用午餐时间学习函数编程?不行,万一他们发现我什么都不懂,怎么办?
与其他团队领导会面,问问他们是否对函数编程感兴趣,看看他们还有什么更好的方法,或者听听他们不感兴趣的原因?不行,这事儿得向经理报告,他们会认为我太天真或太能折腾,那我岂不是得不偿失?
将自己所学的技术私藏起来,其他团队无法做出贡献,也无法改善现状。
宁可使用错误的抽象,也不采用错误的重复
在 2014 年美国芝加哥举行的 RailsConf 大会上,Sandi Metz 发表的演讲(https://www.youtube.com/watch?v=8bZh5LMaSmE&ab_channel=Confreaks)给我留下了深刻的印象,她认为“即使不得不重复,也不要使用错误的抽象”。两年后,她在博客上发表了一些很棒的评论“错误的抽象”(https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction)。
她认为,提取核心的业务逻辑,反复抽象为广泛的概念,最后将导致没有人能充分理解这些概念,更没有人了解这些抽象如何工作。
如果没人能理解这些抽象,PR的审核也就变成了单纯的点赞,那么很快就会人员流失了。
不重构不适合团队的旧代码
旧代码的唯一用途,就是告诉刚刚加入团队的成员,你们当年的代码是多么不堪。这些代码早就该被重写,但很多人把时间和精力都放在了编写新功能上。
用 compose 连接所有 API 调用,加大调试难度
这段代码看上去似乎还不错:
// handler for POST /posts
import { createPost } from 'app/db/posts'
import { authenticateUser, authorizeUser } from 'app/lib/auth'
import { trackEvent } from 'app/lib/tracking'
const validateRequestSchema = payload => { /* … */ }
export const handleCreatePost = curry(metadata =>
pipeP(
authenticateUser(metadata),
authorizeUser(metadata),
validateRequestSchema,
createPost(metadata),
tapP(trackEvent('post:create', metadata)),
pick([ 'id', 'authorId', 'title' ])
)
)
但是你能发现这段代码需要两个参数吗?你知道 authenticateUser 会忽略第二个参数吗?怎么知道?trackEvent 呢?它接受 payload 吗?或者 createPost() 会返回跟帖子有关的数据?
改成这样就好多了:
export async function handleCreatePost(metadata, payload) {
await authenticateUser(metadata)
await authorizeUser(metadata, payload)
await validateRequestSchema(payload)
const post = await createPost(metadata, payload)
await trackEvent('post:create', metadata, payload)
return {
id: post.id,
authorId: post.authorId,
title: post.title,
}
}
我并不是说第二种写法比第一种写法好,但第一种写法确实会让人摸不着头脑。
重新发明过程式编程,并称之为“声明式”
const setBookReadPercentByType = (contentType, statusObject) =>
assoc(
'readPercent',
pipe(
prop('subItems'),
values,
filter(propEq(contentType, 'chapter')),
length,
flip(divide)(compose(length, keys, prop('subItems'))(statusObject)),
multiply(100),
Math.round
)(statusObject),
statusObject
)
使用大量函数组合的模式
比如下面就是四种函数组合模式,每种还可以写成Promise版本,再加上它们之间的各种组合,这还没提到使用pipeWith、composeWith等。
// 👇 These 4, plus Promisified versions of them,
// plus combinations of them all used at once;
// doesn't include ramda's pipeWith and composeWith
// compose
const getHighScorers =
compose(
mapProp('name'),
takeN(3),
descBy('score')
)
// pipe
const getHighScorers =
pipe(
descBy('score'),
takeN(3),
mapProp('name')
)
// composeWithValue
const getHighScorers = players =>
composeWithValue(
mapProp('name'),
takeN(3),
descBy('score'),
players
)
// pipeWithValue
const getHighScorers = players =>
pipeWithValue(
players,
descBy('score'),
takeN(3),
mapProp('name')
)
// …but then now mix and match them with actual,
// real-life business logic.
代码里写满晦涩难懂的代数操作符
在代码里使用这些代数操作符,肯定会让你的队友一头雾水:
-
Task, Maybe, Either, Result, Pair, State
-
bimap
-
chain
-
bichain
-
option
-
coalesce
-
fork
-
sequence
-
ap
-
map — 我指的不是 Array.prototype.map, 也不是 new Map(), 也不是键值对象的那个map
自己转换数据,而不是让SQL完成
本来 SQL 非常适合转换数据,但非要以“不可变数据”为名,建立数据流水线,在流水线中逐步转换,用这种方式来尽可能能消耗内存。
在同事的 PR 中建议遵循你的函数式风格
“这段代码非常好,要是把所有参数都反过来、删掉中间变量,然后将这些操作都映射到Either上怎样?”
或者
“我注意到你在函数中明确地构造了这些对象。要是能使用<某个函数>,就能定义输出对象的形状,然后把函数作为值来查找或计算每个值。”
还有一些…
-
分享一些初学者看不懂的函数式编程的文章,让他们觉得自己很差劲;
-
持续用函数式编写代码,虽然整个团队里没有第二个人这么做;
-
消极地使用表情符号给别人的PR写评论;
-
在公司发表“函数式编程”的演讲,把你的错误传递到整个公司。
总结
上面提到的许多错误表面上是经验不足、缺乏技术领导力造成的。但我认为,真实原因应该更深。
最后,我们不能抛弃函数式编程的核心原则:
-
不变性:重新创建对象,而非修改原来的对象,以获得更好的性能;
-
纯函数:使用相同的参数调用相同的函数,得到相同的结果;
-
将副作用放到程序的逻辑边缘上;
-
类很少,没有继承,没有map/filter/reduce 等。
本文文字及图片出自 CSDN
你也许感兴趣的:
- 译 | 实用的函数式编程
- 2分钟编程技巧:不要在代码中使用循环
- 函数式编程中的 “函数们”
- 阮一峰:函数式编程入门教程
- 如何读懂并写出装逼的函数式代码
- 函数式编程中的一些专业术语的详细介绍
- 具有魔法的 H.264
- 多用户环境中的 rootless Docker
- 【外评】微软的人工智能聊天机器人将 “回忆 “您在其新 PC 上所做的一切
- 【外评】苹果需要解释重新出现已删除照片的错误
你对本文的反应是: