函數範式入門(惰性求值與函數式狀態)

第二節 惰性求值與函數式狀態


在下面的代碼中咱們對List數據進行了一些處理java

List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3)

考慮一下這段程序是如何求值的,若是咱們跟蹤一下求值過程,步驟以下:git

List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3)

List(11,12,13,14).filter(_ % 2 == 0).map(_ * 3)

List(12,14).map(_ * 3)

List(36,42)

咱們調用map和filter函數的時候,每次都會遍歷自身,對每一個元素使用輸入的函數進行求值。github


那麼若是咱們對一系列的轉換融合爲單個函數而避免產生臨時數據效率不是更高?算法

咱們能夠在一個for循環中手工完成,但更好的方式是可以自動實現,並保留高階組合風格。編程

非嚴格求值(惰性求值) 便是實現這種的產物,它是一項提高函數式編程效率和模塊化的基礎技術。性能優化


嚴格與非嚴格函數

非嚴格求值是一種屬性,稱一個函數是非嚴格求值便是這個函數能夠選擇不對它的一個參數或多個參數求值。相反,一個嚴格求值函數老是對它的參數求值。
嚴格求值函數在大部分編程語言中是一種常態,而Haskell語言就是一個支持惰性求值的例子。Haskell不能保證任何語句會順序執行(甚至徹底不會執行到),由於Haskell的代碼只有在須要的時候纔會被執行到。app

嚴格求值的定義

若是對一個表達式的求值一直運行或拋出一個錯誤而非返回一個定義的值,咱們說這個表達式沒有結束,或者說它是evaluates to bottom(非正常返回)。
若是表達式f(x)對全部的evaluates to bottom的表達式x,也是evaluates to bottom,那麼f是嚴格求值dom


實際上咱們常常接觸非嚴格求值,好比&&||操做符都是非嚴格求值函數編程語言

scala> false && { println("!!");true }
res0: Boolean = false

而另外一個例子是if控制結構:ide

if(input.isEmpty()) sys.error("empty") else input

if語句能夠看做一個接收3個參數的函數(實際上Clojure中if就是這樣一個函數),一個Boolean類型的條件參數,一個返回類型爲A的表達式在條件爲true時執行,另外一個一樣返回類型爲A的表達式在條件爲false時執行。


所以能夠說,if函數對條件參數是嚴格求值,而對分支參數是非嚴格求值。

if_(a < 22,
    () -> println("true"),
    () -> println("false"))

而一個表達式的未求值形式咱們稱爲thunk

Scala中提供一種語法能夠自動將表達式包裝爲thunk:

def if_[A](cond: Boolean, onTrue: => A, onFalse: => A): A =
    if(cond) onTrue else onFalse

這樣,在使用和調用的時候都不須要作任何特殊的寫法scala會爲咱們自動包裝:

if_(false, sys.error("fail"), 3)

默認狀況下以上包裝的thunk在每次引用的時候都會求值一次:

scala> def maybeTwice(b: Boolean, i => int) = 
    if(b) i+i else 0
scala> val x = maybeTwice(true, { println("hi"); 42})
hi
hi
x: Int = 84

因此Scala也提供一種語法能夠延遲求值並保存結果,使後續引用不會觸發重複求值

scala> def maybeTwice(b: Boolean, i => int) = {
     |   lazy val j = i
     |   if(b) j+j else 0
     | }
scala> val x = maybeTwice(true, { println("hi"); 42})
hi
x: Int = 84

Kotlin在1.1版本中經過委託屬性的方式提供了這種特性的實現


惰性列表

定義:

sealed class Stream<out A> {
    abstract val head: () -> A
    abstract val tail: () -> Stream<A>
    
    object Empty : Stream<Nothing>() {...}
    data class Cons<A>(val h: () -> A, 
                       val t: () -> Stream<A>) 
                       : Stream<A>() {...}
    ...
}
sealed class List<out A> {
    abstract val head: A
    abstract val tail: List<A>
    
    object Nil : List<Nothing>() {...}
    data class Cons<out A>(val head: A,
                           val tail: List<A>) 
                           : List<A>() {...}
    ...
}

eg:

Stream.apply(1, 2, 3, 4)
        .map { it + 10 }
        .filter { it % 2 == 0 }
        .toList()

toList()方法請求數據,而後請求到1,而後通過一系列計算回到toList();而後請求下一個數據,依次類推直到無數據可取toList()方法結束。


無限流與共遞歸

eg:

fun ones(): Stream<Int> = Stream.cons({ 1 }, { ones() })

fun <A> constant(a: A): Stream<A> = 
    Cons({ a }, { constant(a) })
val fibs = {
    fun go(f0: Int, f1: Int): Stream<Int> =
            cons({ f0 }, { go(f1, f0+f1) })
    go(0, 1)
}

unfold函數

fun <A, S> unfold(z: S, f: (S) -> Option<Pair<A, S>>)
    : Stream<A> {
    val option = f(z)
    return when {
        option is Option.Some -> {
            val h = option.get.first
            val s = option.get.second
            cons({ h }, { unfold(s, f) })
        }
        else -> empty()
    }
}

彷佛和Iterator很像,但這裏返回的是一個惰性列表,而且沒有可變量


共遞歸有時也稱爲 守護遞歸,生產能力有時也稱爲 共結束

函數式編程的主題之一即是關注分離,但願能將計算的描述實際運行分開。好比

  • 一等函數:函數體內的邏輯只有在接收到參數的時候才執行
  • Either:將捕獲的錯誤如何對錯誤進行處理進行分離
  • Stream:將如何產生一系列元素的邏輯和實際生成元素進行分離

惰性計算即是實踐這一思想。


惰性計算是隻有在必要時才進行計算當然在不少方面都提供了好處(無限列表、計算延遲等等),但這也意味着對函數的純淨度有更高的要求:
因爲不肯定傳入的函數何時執行、按什麼順序執行(在性能優化的時候進行指令重排)、甚至會不會執行,若是傳給map或者filter的函數是有反作用的,就會使程序狀態變得不可控。

eg:

public Adapter<String> getAdapter() {
    BaseAdapter adapter = new ListAdapter(getContext());
    
    presenter.getListData()
        .map(m -> m.getName())
        .subscribe(adapter::setData);
        
    return adapter;
}

public Observable<Adapter<String>> getAdapter() {
    return Observable.zip(
    
            Observable.just(getContext())
                    .map(c -> new ListAdapter(c)),
                    
            presenter.getListData()
                    .map(m -> m.getName()),
                    
            (adapter, data) -> adapter.setData(data));
}

public Observable<Adapter<String>> getAdapter(
    Context context,
    Observable<DataModel> dataProvider) {
    return Observable.zip(
    
            Observable.just(context)
                    .map(c -> new ListAdapter(c)),
                    
            dataProvider
                    .map(m -> m.getName()),
                    
            (adapter, data) -> adapter.setData(data));
}

純函數式狀態

eg:
隨機數生成器:

public static int main(String[] args) {
    Random random = new Random();
    System.out.println(random.nextInt());
    System.out.println(random.nextInt());
    System.out.println(random.nextInt());
}

很明顯,原有的Random函數不是引用透明的,這意味着它難以被測試、組合、並行化。


好比如今有一個函數模擬6面色子

fun rollDie(): Int {
    val rng = Random()
    return rng.nextInt(6)
}

咱們但願它能返回1~6的值,然而它返回的是0~5,在測試時有必定可能會失敗,然而在失敗時但願重現失敗也不切實際。


也許咱們能夠經過傳入指定的Random對象保證一致的生成:

fun rollDie(rng : Random): Int {
    return rng.nextInt(6)
}

但調用一次nextInt方法後Random上一次的狀態就丟失了,這意味着咱們還須要傳一個調用次數的參數?

不,咱們應該避開反作用


定義:

interface Rng {
  fun nextInt: (Int, Rng)
}
class LcgRng(val seed: Int = System.currentTimeMillis())
: Rng {
    //線性同餘算法
    fun nextInt(): Pair<Int, Rng> {
        val newSeed = (seed * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL;
        val nextRng = LcgRng(newSeed)
        val n = (newSeed >>> 16).toInt();
        return Pair(n, nextRng);
    }
}

更加範化今生成器咱們能夠定義:

type Rand[+A] = RNG => (A, RNG)

基於這個隨機數生成器咱們能夠進行更加靈活的組合:

val int: Rand[Int] = _.nextInt

def map[A,B](s: Rand[A])(f: A => B): Rand[B]

def double(rng: RNG): (Double, RNG)

def map2[A,B,C](ra: Rand[A], rb: Rand[B], 
    f: (A, B) => C): Rand[C]

def flatMap[A,B](f: Rand[A])(g: A => Rand[B]): Rand[B]

更爲通用的狀態行爲數據類型:
scala

case class State[S, +A](run: S => (A, S))

kotlin

class State<S, out T>(val run: (S) -> Pair<T, S>)

java

public final class State<S, A> {
    private final F<S, P2<S, A>> runF;
}

for推導

val ns: Rand[List[Int]] =
    //int爲Rand[Int]的值,生成一個隨機整數
    int.flatMap(x => 
        //ints(x)生成一個長度爲x的list
        ints(x).map(xs =>
            //list中的每一個元素變換爲除以y的餘數
            xs.map(_ % y))))

for推導語句:

val ns: Rand[List[Int]] = for {
    x <- int
    y <- int
    xs <- ints(x)
} yield xs.map(_ % y)

相似Haskell的Do語句
for推導使得書寫風格更加命令式、更加易懂

Avocado(爲Java中實現do語法的小工具)

DoOption
        .do_(() -> Optional.ofNullable("1"))
        .do_(() -> Optional.ofNullable("2"))
        .do_12((x, y) -> Optional.ofNullable(x + y))
        .return_123((t1, t2, t3) -> t1 + t2 + t3)
        .ifPresent(System.out::println);

經典問題:

實現一個對糖果售貨機建模的有限狀態機。機器有兩種輸入方式:能夠投入硬幣,或能夠按動按鈕獲取糖果。它能夠有一種或兩種狀態:鎖定或非鎖定。它也能夠跟蹤還剩多少糖果以及有多少硬幣。

機器遵循下面的規則:

  • 對一個鎖定狀態的售貨機投入一枚硬幣,若是有剩餘的糖果它將變爲非鎖定狀態。
  • 對一個非鎖定狀態的售貨機按下按鈕,它將給出糖果並變回鎖定狀態。
  • 對一個鎖定狀態的售貨機按下按鈕或對非鎖定狀態的售貨機投入硬幣,什麼也不作。
  • 售貨機在「輸出」糖果時忽略全部「輸入」

sealed trait Input
case object Coin extends Input
case object Turn extends Input

case class Machine(locked: Boolean, candies: Int,
                   coins: Int)

object Candy {
  def update = (i: Input) => (s: Machine) =>
    (i, s) match {
      case (_, Machine(_, 0, _)) => s
      case (Coin, Machine(false, _, _)) => s
      case (Turn, Machine(true, _, _)) => s
      case (Coin, Machine(true, candy, coin)) =>
        Machine(false, candy, coin + 1)
      case (Turn, Machine(false, candy, coin)) =>
        Machine(true, candy - 1, coin)
    }

  def simulateMachine(inputs: List[Input])
      : State[Machine, (Int, Int)] = for {
    _ <- sequence(inputs map (modify[Machine] _ compose update))
    s <- get
  } yield (s.coins, s.candies)
}

本章知識點:

  1. 惰性求值
  2. 函數式狀態

To be continued

相關文章
相關標籤/搜索