在函数式编程中实现状态

时间:2017-01-14 11:58:53

标签: functional-programming

我目前正在使用Scala编写Android音乐播放器应用程序。我选择了Scala的功能编程功能,我想让代码尽可能符合FP。

由于FP意味着不变性,代码不应该携带任何状态,变量应该是不可变的。但我面对一些复杂的用例,我不知道如何以纯函数式编程方式解决。

第一个是播放列表案例。音乐播放器正在播放播放列表中间的歌曲。这可以用歌曲列表和指示当前播放歌曲的光标来表示。但是当歌曲结束时,播放器必须播放下一首歌曲,因此,更改光标的值。

播放列表本身也会出现同样的问题:用户必须能够更改(添加或禁止歌曲)播放列表。如果播放列表本身是不可变的,则只要用户添加或抑制歌曲,就会产生新的播放列表。但是该播放列表必须受到必须可变的变量的影响。

我在这个应用程序中看到的每个地方,我看到状态 - 玩家是否暂停了?当前的歌曲是什么,当前的播放列表?设置的当前状态是什么?等等 - 而且我不知道如何用纯函数式编程方式解决这个问题,使用不可变变量。

由于这些用例看起来很标准,我想有一些设计模式可以解决它们(比如monad),但我不知道在哪里看。

2 个答案:

答案 0 :(得分:1)

我写了一些试图解决这个问题的图书馆,结果相当丑陋,IMO。

基本上,将Activity,Fragment等转换为接受State并返回State的纯函数。

这与IO monads结合使得界面有点纯粹。下面是一个示例(PureActivity的源代码可以在https://github.com/pfn/iota-pure找到),在这种情况下,'state'是'Option [Process]',当logcat运行时Process存在,而当logcat运行时则为空。没有vars:

class LogcatActivity extends AppCompatActivity with PureActivity[Option[Process]] {
  val LOG_LINE = """^([A-Z])/(.+?)\( *(\d+)\): (.*?)$""".r
  val buffersize = 1024
  lazy val toolbar = newToolbar
  lazy val recycler = {
    val r = new RecyclerView(this)
    r.setLayoutManager(new LinearLayoutManager(this))
    r.setAdapter(Adapter)
    r
  }
  lazy val layout = l[LinearLayout](
    toolbar.!  >>= lp(MATCH_PARENT, WRAP_CONTENT),
    recycler.! >>= lp(MATCH_PARENT, 0, 1)
  ) >>= vertical

  override def initialState(b: Option[Bundle]) = None

  override def applyState[T](s: ActivityState[T]) = s match {
    case OnPreCreate(_) => s(IO(
      setTheme(if (Settings.get(Settings.DAYNIGHT_MODE)) R.style.SetupTheme_Light else R.style.SetupTheme_Dark)
    ))
    case OnCreate(_) => s(IO {
      toolbar.setTitle("Logcat")
      toolbar.setNavigationIcon(resolveAttr(R.attr.qicrCloseIcon, _.resourceId))
      toolbar.navigationOnClick0(finish())
      setContentView(layout.perform())
    })
    case OnStart(_) => s.applyState(IO {
      var buffering = true
      val logcat = "logcat" :: "-v" :: "brief" :: Nil
      val lineLogger = new ProcessLogger {
        override def out(s: => String) = addLine(s)
        override def buffer[X](f: => X) = f
        override def err(s: => String) = addLine(s)

        def addLine(line: String) = line match {
          case LOG_LINE(level, tag, pid, msg) =>
            if (tag != "ResourceType") UiBus.run {
              val c = Adapter.getItemCount // store in case at max items already
              Adapter.buffer += LogEntry(tag, level, msg)
              Adapter.notifyItemInserted(math.min(buffersize, c + 1))
              if (!buffering)
                recycler.smoothScrollToPosition(Adapter.getItemCount)
            }
          case _ =>
        }
      }
      Future {
        Thread.sleep(500)
        buffering = false
      } onSuccessMain { case _ =>
        recycler.scrollToPosition(Adapter.getItemCount - 1)
      }
      logcat.run(lineLogger).?
    })
    case OnStop(proc) => s.applyState(IO {
      proc.foreach(_.destroy())
      None
    })
    case x => defaultApplyState(x)
  }

  case class LogEntry(tag: String, level: String, msg: String)
  case class LogcatHolder(view: TextView) extends RecyclerView.ViewHolder(view) {
    def bind(e: LogEntry): Unit = view.setText(" %1 %2: %3" formatSpans (
      textColor(MessageAdapter.nickColor(e.level), e.level),
      textColor(MessageAdapter.nickColor(e.tag),   e.tag), e.msg))
  }
  object Adapter extends RecyclerView.Adapter[LogcatHolder] {
    val buffer = RingBuffer[LogEntry](buffersize)
    override def getItemCount = buffer.size
    override def onBindViewHolder(vh: LogcatHolder, i: Int) = vh.bind(buffer(i))

    override def onCreateViewHolder(viewGroup: ViewGroup, i: Int) = {
      val tv = new TextView(LogcatActivity.this)
      tv.setTypeface(Typeface.MONOSPACE)
      LogcatHolder(tv)
    }
  }
}

答案 1 :(得分:1)

您正在谈论用户界面。它本质上是有状态的。没有州,你不能也不能使用它。只有一种正确的方法:将没有状态的代码与具有状态的代码分开。

最好的概念是FRP - Functional reactive programming。它将功能部分和不可变框与可变状态内容分开,并通过事件连接它们。

要小心,网上的许多所谓的反应式编程技术实际上并非如此,只是宣称是被动的。例如,java RX绝对无效,缺少两个非常重要功能。 (隐藏听众和同时支持)

关于这个问题有一个非常好的book。它也可以在网上找到一些动作。作者给出了开源基础库和swift FRP支持库,可以将其用作创建自己的FRP类的模式,以满足您的需求。