【问题标题】:How to translate this javascript code into functional way (better using Ramdajs)?如何将此javascript代码转换为功能方式(更好地使用Ramdajs)?
【发布时间】:2017-03-26 05:53:26
【问题描述】:

我正在尝试创建带有界面设置模块,它可以提供更改和读取设置对象的功能。

现在代码如下所示:

let _settings = {
    user: {
        id: userId || '',
        authorized: false,
    }
}

function getSettings() {
    return _settings
}

function addParamToSettings(param) {
    _settings = {
        ..._settings,
        ...param,
    }
}

function addParamToUser(param) {
    addParamToSettings({
        user: {
            ...getSettings().user,
            ...param,
        },
    })
}

let saveUserId = function saveUserId(id) {
    addParamToUser({ id, authorized: true })
}

我想用更实用的方式重写这个(比如默认样式,使用镜头,不可变等),更好地使用 Ramda js

首先我不明白如何以函数方式工作,然后你需要某种可以读取和写入的信息存储(如本例中的对象 _settings)。

我尝试重写 addParamToApp 函数,最好的结果是:

const userLense = R.lensProp('user')

const _addParamToUser = R.compose(R.set(userLense, R.__, _settings), R.merge(_settings.user))

但我仍然需要为句柄 _settings 更改编写函数

const addParamToUser = function addParamToUser(param){
    _settings = _addParamToUser(param)
}

这在我看来是不对的。还有比第一次实现更多的代码。

如何以更实用的方式编写它?读写函数如何处理信息存储?

【问题讨论】:

  • 不要添加功能特性只是为了勾选功能清单上的框。 为什么你觉得你需要改变当前的代码?太难测试/推理?我要做的唯一改变是你的函数除了用一些预设参数调用其他函数之外什么都不做。请改用.apply 和/或compose
  • 您的代码只是试图用idauthorized 覆盖_settings 变量,是不是太简化了?为什么不只是 R.set?
  • @JaredSmith 我自己说的再好不过了
  • @naomik 谢谢。我喜欢 FP,但有时觉得最近的 FP 转换就像这家伙 circleci.com/blog/its-the-future

标签: javascript functional-programming ramda.js


【解决方案1】:

国家单子

您可能有兴趣探索 State monad。我将先介绍程序的几个部分,然后在底部包含一个完整的可运行示例。

首先,我们将介绍 State monad。网上有无数的 monad 介绍超出了这篇文章的范围,所以我将只介绍足以让你启动和运行的内容。 State monad 的其他实现将有额外的便利;如果您有兴趣了解更多信息,请务必了解这些内容。

// State monad
const State = runState => ({
  runState,
  bind: f => State(s => {
    let { value, state } = runState(s)
    return f(value).runState(state)
  }),
  evalState: s =>
    runState(s).value,
  execState: s =>
    runState(s).state
})

State.pure = y =>
  State(x => ({ value: y, state: x }))

State.get = () =>
  State(x => ({ value: x, state: x }))

State.put = x =>
  State($ => ({ value: null, state: x }))

规则优先

与所有单子一样,要成为单子,它必须满足以下三个单子定律。

  1. 左身份pure(a).bind(f) == f(a)
  2. 正确身份m.bind(pure) == m
  3. 关联性m.bind(f).bind(g) == m.bind(x => f(x).bind(g))

pure 是我们将值放入 M​​onad 实例的方式,mbind 是我们与实例包含的值进行交互的方式 - 你有时会在这里将 pure 称为 @ 987654332@ 在 monad 的其他描述中,但我将避免在此答案中使用 return,因为它是 JavaScript 中的关键字,与 monad 无关。

不要太担心理解实现细节。刚开始时,最好对 State monad 的如何工作有一个直觉。稍后我们会看到这一点,因为我们现在看一个使用 state monad 的示例程序


你的第一个有状态函数

假设您的初始程序状态是{},我们将查看一个修改状态以添加user 和对应的userId 的函数。这可能是在数据库中查找用户的结果,我们将authorized 属性设置为false,直到我们稍后验证用户的密码是否正确

const setUser = userId =>
  State.get().bind(settings =>
    State.put(Object.assign({}, settings, {
      user: { userId, authorized: false }
    })))

此函数的第 1 步是使用 State.get() 获取状态 - 就其本身而言,它似乎什么都不做,但正是在调用此函数的上下文中,它产生了明显的不同。稍后您将看到我们使用初始状态值执行计算的位置,State.get 似乎凭空出现。同样,现在,只需对它有一个直觉,并假设我们以某种方式得到了状态。

第 2 步是 bind(settings => ... 位。回想一下bind 是我们与状态值交互的方式,在这种情况下,它是我们的settings 对象。所以我们在这里真正想做的就是更新settings 以包含一个设置为{ userId, authorized: false }user 属性。在setUser被调用之后,我们可以认为我们的状态有一个新的形状

// old state
let settings = {...}

// new state
settings = { ...settings, { user: { userId, authorized: false } } }

第二个有状态函数

好的,您的用户现在想登录。让我们接受challenge 并将其与某个密码进行比较——如果用户提供了正确的密码,我们将更新状态以显示authorized: true,否则我们将其设置为false

const PASSWORD = 'password1'

const attemptLogin = challenge =>
  State.get().bind(settings => {
    let { userId, authorized } = settings.user
    if (challenge === PASSWORD)
      return State.put(Object.assign({}, settings, {
        user: { userId, authorized: true }
      }))
    else
      return State.pure(settings)
  })

第一步是再次获取状态,就像上次一样

第 2 步是 bind 以访问状态值并一些事情。回想一下我们在之前的状态修改中离开的地方(上一节末尾的new state):要读取用户的当前状态,我们要读取settings.user

第3步是决定下一个状态是什么:如果用户提供了正确的challenge(即等于PASSWORD),我们将返回一个新的状态,将authorized设置为@987654360 @ – 否则,如果质询与密码不匹配,则使用State.pure(settings)

返回 未修改 状态

您的第一个有状态程序

所以现在我们有两个函数(setUserattemptLogin)读取状态并返回它们自己的状态。现在编写我们的程序很容易

const initialState = {}

const main = (userId, challenge) => 
  setUser(userId)
    .bind(() => attemptLogin(challenge))
    .execState(initialState)

就是这样。运行下面的完整代码示例,查看两种登录场景的输出:一种密码有效,另一种密码无效


完整的代码示例

// State monad
const State = runState => ({
  runState,
  bind: f => State(s => {
    let { value, state } = runState(s)
    return f(value).runState(state)
  }),
  evalState: s =>
    runState(s).value,
  execState: s =>
    runState(s).state
})

State.pure = y =>
  State(x => ({ value: y, state: x }))

State.get = () =>
  State(x => ({ value: x, state: x }))

State.put = x =>
  State($ => ({ value: null, state: x }))

// your program
const PASSWORD = 'password1'
const initialState = {}

const setUser = userId =>
  State.get().bind(settings =>
    State.put(Object.assign({}, settings, {
      user: { userId, authorized: false }
    })))
    
const attemptLogin = challenge =>
  State.get().bind(settings => {
    let { userId, authorized } = settings.user
    if (challenge === PASSWORD)
      return State.put(Object.assign({}, settings, {
        user: { userId, authorized: true }
      }))
    else
      return State.pure(settings)
  })
  
const main = (userId, challenge) => 
  setUser(userId)
   .bind(() => attemptLogin(challenge))
   .execState(initialState)

// good login
console.log(main(5, 'password1'))
// { user: { userId: 5, authorized: true } }

// bad login
console.log(main(5, '1234'))
// { user: { userId: 5, authorized: false } }

从这里到哪里去?

这只是触及了状态单子的表面。我花了很长时间才对它的工作原理有了一些直觉,但我仍然没有掌握它。

如果您对它的工作原理摸不着头脑,我强烈建议您采用旧的笔/纸评估策略 - 跟踪程序并谨慎处理替换。当您看到它们融合在一起时,真是太棒了。

并且一定要多读一些来自不同来源的 State monad


“更好地使用 Ramda?”

我认为您的部分问题在于您缺乏以功能方式推理程序的一些基础知识。像 Ramda 这样的库不太可能帮助您提高技能或给您更好的直觉。以我自己的经验,我最好通过坚持基础并从那里开始学习。

作为一门学科,您可以在使用任何 Ramda 功能之前练习实现它。只有这样,您才能真正了解/欣赏 Ramda 带来了什么。

【讨论】:

  • @Bergi 在写完这篇文章时,我感到有些不安。如果没有实现太多以至于读者会因细节而窒息,就很难走这条线来显示足够的输出——我特别不愿意将 return 重命名为 pure 以避免 JavaScript 中的 return 关键字.如果您对改进此答案有任何建议,我将不胜感激^_^
  • 写得好!使用(或包含)幻想世界的命名约定可能会有所帮助。所以pure 变成ofbind 变成chain。特别是如果 OP 对 ramda 生态系统感兴趣。
  • 非常感谢您的回答!但是,我仍然不明白如何在我的代码后面从 main 函数中获取结果,例如在用户执行一些操作之后?
  • @Dionid 是的,你有一个复杂的问题,在准备好使用函数式编程技术解决它之前,你需要了解很多。如果你有兴趣,你会想看看Future monad 和StateT monad 转换器。我仍在学习如何以函数式的方式表达异步程序,所以我不是带你走得更远的最佳人选。我还可以建议使用类似redux 的横向方法,它会以非常系统的方式改变状态。
  • @naomik 我已经非常熟悉 Redux 并且我已经使用它创建了这个平台(它在没有 ui 的代码中非常好用,但需要更多接口来模仿 mapStateToProps 和订阅状态变化的模块)。
猜你喜欢
  • 2020-09-02
  • 1970-01-01
  • 1970-01-01
  • 2023-03-20
  • 2021-12-24
  • 1970-01-01
  • 1970-01-01
  • 2021-08-01
  • 2020-05-07
相关资源
最近更新 更多