Jumping with Option

weixin_33807284發表於2016-03-10

Billion-Dollar Mistake

Tony Hoare, null的發明者在2009年公開道歉,並將此錯誤稱為Billion-Dollar Mistake

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Idioms and Patterns

Preconditions

絕大多數public的函式對於傳遞給它們的引數都需要進行限制。例如,索引值不能為負數,物件引用不能為空等等。良好的設計應該保證“發生錯誤應儘快檢測出來”。為此,常常會在函式入口處進行引數的合法性校驗。

為了消除大量引數前置校驗的重複程式碼,可以提取公共的工具類庫,例如:

public final class Precoditions {
  private Precoditions() {
  }

  public static void checkArgument(boolean exp, String msg = "") {
    if (!exp) {
      throw new IllegalArgumentException(msg);
    }
  }
  
  public static <T> T requireNonNull(T obj, String msg = "") {
    if (obj == null)
      throw new NullPointerException(msg);
    return obj;
  }

  public static boolean isNull(Object obj) {
    return obj == null;
  }

  public static boolean nonNull(Object obj) {
    return obj != null;
  }
}

使用requireNonNull等工具函式時,常常import static,使其更具表達力。

import static Precoditions.*;

系統中大量存在前置校驗的程式碼,例如:

public BigInteger mod(BigInteger m) {
  if (m.signum() <= 0)
    throw new IllegalArgumentException("must be positive: " + m);
  ...
}

可以被重構得更加整潔、緊湊,且富有表現力。

public BigInteger mod(BigInteger m) {
  checkArgument(m.signum() > 0 , "must be positive: " + m);
  ...
}
checkArgument(count > 0, "must be positive: %s", count);</pre>

一個常見的誤區就是:對所有引數都進行限制、約束和檢查。我將其稱為“缺乏自信”的表現,因為在一些場景下,這樣的限制和檢查純屬多餘。

C++為例,如果public介面傳遞了指標,對該指標做前置校驗無可厚非,但僅僅在此做一次校驗,其在內部呼叫鏈上的所有private子函式,如果要傳遞此指標,應該將其變更為pass by reference;特殊地,如果是隻讀,為了做到編譯時的安全,pass by const-reference更是明智之舉。

可以得到一個推論,對於private的函式,你對其呼叫具有完全的控制,自然保證了其傳遞引數的有效性;如果非得對其private的引數進行前置校驗,應該使用assert。例如:

private static void <T> sort(T a[], int offset, int length) {
  assert a != null;
  assert offset >= 0 && offset <= a.length;
  assert length >= 0 && length <= a.length - offset;
  
  ...
}

Avoid Pass/Return Null

private final List<Product> stock = new ArrayList<>();

public Product[] filter(Predicate<Product> pred) {
  if (stock.isEmpty()) return null;
  ...
}

客戶端不得不為此校驗返回值,否則將在執行時丟擲NullPointerException異常。

Product[] fakes = repo.filter(Product::isFake);
if (fakes != null && Arrays.asList(fakes).contains(Product.STILTON)) {
  ...
}

經過社群的實踐總結出,返回null的陣列或列表是不明智的,而應該返回零長度的陣列或列表。

private final List<Product> stock = new ArrayList<>();

private static final Product[] EMPTY = new Product[0]; 

public Product[] filter(Predicate<Product> pred) {
  if (stock.isEmpty()) return EMPTY;
  ...
}

對於返回值是List的,則應該使用Collections.emptyXXX的靜態工廠方法,返回零長度的列表。

private final List<Product> stock = new ArrayList<>();

public Product[] filter(Predicate<Product> pred) {
  if (stock.isEmpty()) return Collections.emptyList();
  ...
}

Null Object

private final List<Product> stock = new ArrayList<>();

public Product[] filter(Predicate<Product> pred) {
  if (stock.isEmpty()) return Collections.emptyList();
  ...
}

Collections.emptyList()工廠方法返回的就是一個Null Object,它的實現大致是這樣的。

public final class Collections {
  private Collections() {
  }
 
  private static class EmptyList<E> 
    extends AbstractList<E> 
    implements RandomAccess, Serializable {
  
    private static final long serialVersionUID = 8842843931221139166L;
  
    public Iterator<E> iterator() {
      return emptyIterator();
    }

    public ListIterator<E> listIterator() {
      return emptyListIterator();
    }
  
    public int size() {return 0;}
    public boolean isEmpty() {return true;}
  
    public boolean contains(Object obj) {return false;}
    public boolean containsAll(Collection<?> c) { return c.isEmpty(); }
  
    public Object[] toArray() { return new Object[0]; }
  
    public <T> T[] toArray(T[] a) {
      if (a.length > 0)
        a[0] = null;
      return a;
    }
  
    public E get(int index) {
      throw new IndexOutOfBoundsException("Index: "+index);
    }
  
    public boolean equals(Object o) {
      return (o instanceof List) && ((List<?>)o).isEmpty();
    }
  
    public int hashCode() { return 1; }
    
    private Object readResolve() {
      return EMPTY_LIST;
    }
  }
    
  @SuppressWarnings("rawtypes")
  public static final List EMPTY_LIST = new EmptyList<>();

  @SuppressWarnings("unchecked")
  public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
  }
}    

Null Object代表了一種例外,並且這樣的例外具有特殊性,它是一個有效的物件,對於使用者來說是透明的,是感覺不出來的。使用Null Object,遵循了"按照介面程式設計"的良好設計原則,並且讓使用者處理空和非空的情況得到了統一,使得因缺失null檢查的錯誤拒之門外。

Monadic Option

Null Object雖然很優雅地使得空與非空得到和諧,但也存在一些難以忍受的情況。

  • 介面發生變化(例如新增加一個方法),代表Null Object的類也需要跟著變化;

  • Null Object在不同的場景下重複這一實現方式,其本質是一種模式的重複;

  • 有時候,引入Null Object使得設計變得更加複雜,往往得不償失;

Option的引入

問題的本質在哪裡?null代表的是一種空,與其對立的一面便是非空。如果將其放置在一個容器中,問題便得到了很完美的解決。也就是說,如果為空,則該容器為空容器;如果不為空,則該值包含在容器之中。

Scala語言表示,可以建立一個Option的容器。如果存在,則用Some表示;否則用None表示。

sealed abstract class Option[+A] {
  def isEmpty: Boolean
  def get: A
}

case class Some[+A](x: A) extends Option[A] {
  def isEmpty = false
  def get = x
}

case object None extends Option[Nothing] {
  def isEmpty = true
  def get = throw new NoSuchElementException("None.get")
}

這樣的表示有如下幾個方面的好處:

  • 對於存在與不存在的值在型別系統中得以表示;

  • 顯式地表達了不存在的語義;

  • 編譯時保證錯誤的發生;

問題並沒有那麼簡單,如果如下使用,並沒有發揮出Option的威力。

def double(num: Option[Int]) = {
  num match {
    Some(n) => Some(n*2)
    None => None
  }
}

Option視為容器,讓其處理Some/None得到統一性和一致性。

def double(num: Option[Int]) = num.map(_*2)

也可以使用for Comprehension,在某些場景下將更加簡潔、漂亮。

def double(num: Option[Int]) = for (n <- num) yield(n*2)

Option的本質

通過上例的可以看出來,Option本質上是一個Monad,它是一種函式式的設計模式。用Java8簡單地形式化一下,可以如下形式化地描述一個Monad

interface M<A> {
  M<B> flatMap(Function<A, M<B>> f);
  
  default M<B> map(Function<A, B> f) {
    return flatMap(a -> unit(f(a)));
  }
  
  static M<A> unit(A a) {
    ...
  }
}

同時滿足以下三條規則:

  • 右單位元(identity),既對於任意的Monad m,則m.flatMap(unit) <=> m

  • 左單位元(unit),既對於任意的Monad m,則unit(v).flatMap(f) <=> f(v)

  • 結合律,既對於任意的Monad m, 則m.flatMap(g).flatMap(h) <=> m.flatMap(x => g(x).flatMap(h))

在這裡,我們將Monad的數學語義簡化,為了更深刻的瞭解Monad的本質,必須深入理解Cathegory Theory,這好比你要吃披薩的烹飪精髓,得學習義大利的文化。但這對於大部分的程式設計師要求優點過高,但不排除部分程式設計師追求極致。

Option的實現

Option的設計與List相似,有如下幾個方面需要注意:

  • Option是一個Immutablity Container,或者是一個函式式的資料結構;

  • sealed保證其型別系統的封閉性;

  • Option[+A]型別引數是協變的,使得None可以成為任意Option[+A]的子物件;

  • 可以被for Comprehension呼叫;

sealed abstract class Option[+A] { self =>
  def isEmpty: Boolean
  def get: A
  
  final def isDefined: Boolean = !isEmpty

  final def getOrElse[B >: A](default: => B): B =
    if (isEmpty) default else this.get

  final def map[B](f: A => B): Option[B] =
    if (isEmpty) None else Some(f(this.get))

  final def flatMap[B](f: A => Option[B]): Option[B] =
    if (isEmpty) None else f(this.get)

  final def filter(p: A => Boolean): Option[A] =
    if (isEmpty || p(this.get)) this else None

  final def filterNot(p: A => Boolean): Option[A] =
    if (isEmpty || !p(this.get)) this else None

  final def withFilter(p: A => Boolean): WithFilter = new WithFilter(p)

  class WithFilter(p: A => Boolean) {
    def map[B](f: A => B): Option[B] = self filter p map f
    def flatMap[B](f: A => Option[B]): Option[B] = self filter p flatMap f
    def foreach[U](f: A => U): Unit = self filter p foreach f
    def withFilter(q: A => Boolean): WithFilter = new WithFilter(x => p(x) && q(x))
  }

  final def foreach[U](f: A => U) {
    if (!isEmpty) f(this.get)
  }

  final def collect[B](pf: PartialFunction[A, B]): Option[B] =
    if (!isEmpty) pf.lift(this.get) else None

  final def orElse[B >: A](alternative: => Option[B]): Option[B] =
    if (isEmpty) alternative else this
}

case class Some[+A](x: A) extends Option[A] {
  def isEmpty = false
  def get = x
}

case object None extends Option[Nothing] {
  def isEmpty = true
  def get = throw new NoSuchElementException("None.get")
}

for Comprehension的本質

for Comprehension其實是對具有foreach, map, flatMap, withFilter訪問方法的容器的一個語法糖。

首先,pat <- expr的生成器被解釋為:

// pat <- expr
pat <- expr.withFilter { case pat => true; case _ => false }

如果存在一個生成器和yield語句,則解釋為:

// for (pat <- expr1) yield expr2
expr1.map{ case pat => expr2 }

如果存在多個生成器,則解釋為:

// for (pat1 <- expr1; pat2 <- expr2) yield exprN
expr.flatMap { case pat1 => for (pat2 <- expr2) yield exprN }
expr.flatMap { case pat1 => expr2.map { case pat2 =>  exprN }}

對於for loop,可解釋為:

// for (pat1 <- expr1; pat2 <- expr2;...) exprN
expr.foreach { case pat1 => for (pat2 <- expr2; ...) yield exprN }

對於包含guard的生成器,可解釋為:

// pat1 <- expr1 if guard
pat1 <- expr1.withFilter((arg1, arg2, ...) => guard)

Others

  • Stream

  • Promise

  • Either

  • Try

  • Validation

  • Transaction

後需文章將逐一解開它們的面紗,敬請期待!

相關文章