我目前正在使用Scala编写Android音乐播放器应用程序。我选择了Scala的功能编程功能,我想让代码尽可能符合FP。
由于FP意味着不变性,代码不应该携带任何状态,变量应该是不可变的。但我面对一些复杂的用例,我不知道如何以纯函数式编程方式解决。
第一个是播放列表案例。音乐播放器正在播放播放列表中间的歌曲。这可以用歌曲列表和指示当前播放歌曲的光标来表示。但是当歌曲结束时,播放器必须播放下一首歌曲,因此,更改光标的值。
播放列表本身也会出现同样的问题:用户必须能够更改(添加或禁止歌曲)播放列表。如果播放列表本身是不可变的,则只要用户添加或抑制歌曲,就会产生新的播放列表。但是该播放列表必须受到必须可变的变量的影响。
我在这个应用程序中看到的每个地方,我看到状态 - 玩家是否暂停了?当前的歌曲是什么,当前的播放列表?设置的当前状态是什么?等等 - 而且我不知道如何用纯函数式编程方式解决这个问题,即使用不可变变量。
由于这些用例看起来很标准,我想有一些设计模式可以解决它们(比如monad),但我不知道在哪里看。
答案 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类的模式,以满足您的需求。