如何将此 javascript 代码转换为功能性方式(最好使用 Ramdajs)?

How to translate this javascript code into functional way (better using Ramdajs)?

我正在尝试使用 接口 创建 设置模块 ,它可以提供 更改和读取设置的功能 对象。

现在代码如下所示:

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 change

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

我觉得不对。还有比第一次实现更多的代码。

如何以更实用的方式编写它?如何处理具有读写功能的信息存储?

状态单子

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

首先,我们将介绍 State monad。网上有无数的 monad 介绍超出了本文 post 的范围,所以我只介绍足以让你起床 运行 的内容。 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 }))

规则优先

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

  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 是我们将值放入我们的 Monad 实例的方式,mbind 是我们与实例包含的值进行交互的方式 – 你'我有时会在这里 pure 在 monad 的其他描述中称为 return,但我会避免在这个答案中使用 return,因为它是 JavaScript 中的关键字并且与 monad 无关。

不要太担心了解实现细节。刚开始时,最好对 State monad 如何 工作形成直觉。稍后我们将看到使用状态 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 以包含 user 属性 设置为 { userId, authorized: false }。在 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),我们将 return authorized 设置为 true 的新状态 – 否则,如果挑战与密码不匹配,return 一个 未修改的 状态使用 State.pure(settings)


你的第一个有状态 程序

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

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 } }


从这里到哪里去?

这只是状态 monad 的皮毛。我花了很长时间才对它的工作原理有了一些直觉,但我还没有掌握它。

如果您对它的工作原理摸不着头脑,我强烈建议您采用旧的 pen/paper 评估策略 – 跟踪程序并仔细处理您的替换。当你看到这一切融合在一起时,真是太神奇了。

并且一定要在各种来源上多读一些关于 State monad 的文章


"better using Ramda?"

我认为您的部分问题在于您缺乏以函数式方式推理程序的一些基础知识。接触像 Ramda 这样的库不太可能帮助你发展技能或给你更好的直觉。根据我自己的经验,我通过坚持基础知识并从那里建立起来来学习最好。

作为一门学科,您可以在使用之前练习实现任何 Ramda 函数。只有这样,您才能真正 know/appreciate Ramda 带来的东西。