Scala函数论

桃花落,闲池阁,山盟虽在,锦书难托,莫,莫,莫!

「函数(Function)」是函数式编程的基本单元。本文将重点讨论函数式理论中几个容易混淆的概念,最后通过ScalaHamcrest的实战,加深对这些概念的理解和运用。

  1. 函数类型
  2. 函数形态
  3. 柯里化
  4. 高阶函数
  5. 部分应用函数
  6. 偏函数
  7. 实战ScalaHamcrest
  8. 总结

函数类型

Scala中,函数可以使用「函数字面值(Function Literal)」直接定义。有时候,函数字面值也常常称为「函数」,或「函数值」。

(s: String) => s.toLowerCase

匿名函数

事实上,「函数字面值」本质上是一个「匿名函数(Anonymous Function)」。在Scala里,函数被转换为FunctionN的实例。上例等价于:

new Function1[String, String] {
  def apply(s: String): String = s.toLowerCase
}

其中,Function1[String, String]可以简写为String => String,因此它又等价于:

new (String => String) {
  def apply(s: String): String = s.toLowerCase
}

也就是说,「函数字面值」可以看做「匿名函数对象」的一个「语法糖」。

函数值

综上述,函数实际上是FunctionN[A1, A2, ..., An, R]类型的一个实例而已。例如,(s: String) => s.toLowerCaseFunction1[String, String]类型的一个实例。

在函数式编程中,函数做为一等公民,函数值可以被自由地传递和存储。例如,它可以赋予一个变量lower

val lower: String => String = _.toLowerCase

假如存在一个map的柯里化的函数。

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

函数值可以作为参数传递给map函数。

map("HORANCE") { _.toLowerCase }

有名函数

相对于「匿名函数」,如果将「函数值」赋予def,或者val,此时函数常称为「有名函数」(Named Function)。

val lower: String => String = _.toLowerCase
def lower: String => String = _.toLowerCase

两者之间存在微妙的差异。前者使用val定义的变量直接持有函数值,多次使用lower将返回同一个函数值;后者使用def定义函数,每次调用lower将得到不同的函数值。

函数调用

Scala里,「函数调用」实际上等价于在FunctionN实例上调用apply方法。例如,存在一个有名函数lower

val lower: String => String = _.toLowerCase

当发生如下函数调用时:

lower("HORANCE")

它等价于在Function1[String, String]类型的实例上调用apply方法。

lower.apply("HORANCE")

形式化

一般地,函数字面值具有如下形式:

(a1: A1, a2: A2, ..., an: An) => E: R

其中,E是一个具有类型为R的表达式。它的类型为:

(A1, A2, ..., An) => R

或者表示为:

FunctionN[A1, A2, ..., An, R]

其中,函数字面值等价于匿名函数对象的定义:

new FunctionN[A1, A2, ..., An, R] {
  def apply(a1: A1, a2: A2, ..., an: An): R = E
}

其中,FunctionN类型定义为

trait FunctionN[-A1, -A2, ..., -An, +R] {
  def apply(a1: A1, a2: A2, ..., an: An): R
}

高阶函数

接受函数作为参数,或返回函数的函数常常称为「高阶函数(Hign Order Function)」。高阶函数是组合式设计的基础,它极大地提高了代码的可复用性。

例如,starts, ends, contains是三个高阶函数,它们都返回String => Boolean类型的谓词,用于判断某个字符串的特征。

type StringMatcher = String => String => Boolean

def starts: StringMatcher = prefix =>
  _ startsWith prefix

def ends: StringMatcher = suffix =>
  _ endsWith suffix

def contains: StringMatcher = substr =>
  _ contains substr

同样地,高阶函数也可以接受函数类型作为参数。例如,如果要忽略大小写,并复用上面三个函数,可以定义ignoringCase的高阶函数,用于修饰既有的字符串匹配器。

def ignoringCase(matcher: StringMatcher): StringMatcher = substr => 
  str => matcher(substr.toLowerCase)(str.toLowerCase)

可以如下方式使用ignoringCase

assertThat("Horance, Liu", ignoringCase(starts)("horance"))

其中,assertThat也是一个高阶函数。

def assertThat[A](actual: A, matcher: A => Boolean) =
  assert(matcher(actual))

部分应用函数

所谓「部分应用函数」(Partially Applied Function),即至少还有一个参数还未确定的函数。例如,times用于判断m是不是n的整数倍。

def times: Int => Int => Boolean = n => m => m % n == 0

对于常见的倍数关系,可以先确定参数n的值。例如:

val twice  = times(2)
val triple = times(3)

此时,twice, triple是一个Int => Boolean的函数,可以被再次利用。

twice(4)    // true
triple(9)   // true

η扩展

首先,将原来的times进行一下变换。

def times(n: Int)(m: Int): Boolean = m % n == 0

此时,如果采用如下的方式确定twice, triple,将发生错误。

val twice  = times(2)  // Error
val triple = times(3)  // Error

因为,times(2)的真实类型为方法(Method),且拥有(Int)Boolean的类型。它不是函数,且没有函数值。

可以通过在「方法」的后面手动地添加「一个空格和一个_」,将方法转换为一个「部分应用函数」,这种转换常称为「η扩展」。例如:

val twice  = times(2) _
val triple = times(3) _

事实上,times(2) _将生成一个匿名的函数对象。它等价于:

val twice = new Function1[Int, Boolean] { 
  def apply(m: Int): Boolwan = this.times(2)(m)  // 在this对象调用`times`方法
}

自动扩展

如果上下文已经标明函数的类型,编译器会自动实施η扩展。例如:

def atom(matcher: Int => Boolean, action: Int => String): Int => String =
  m => if (matcher(m)) action(m) else ""

因为,atommatcher参数已经标明了函数原型。因此,当传递times(3), to("Fizz")atom时,编译器会自动实施η扩展,将times(3), to("Fizz")转变为一个部分应用函数。

val r_n1 = atom(times(3), to("Fizz"))

它等价于

val r_n1 = atom(times(3) _, to("Fizz") _)

其中,to的实现为:

def to(s: String): Int => String = _ => s

偏函数

JSONObject是一个JSON的一个子类,它持有bindings: Map[String, JSON]toString迭代地处理Map中元组,并通过调用_1, _2方法分别获取当前迭代元组的关键字和值。

case class JSONObject(bindings: Map[String, JSON]) extends JSON {
  override def toString: String = {
    val contents = bindings map { t =>
      "\\\\"" + t._1 + ":" + t._2.toString + "\\\\""
    }
    "{" + (contents mkString ",") + "}"
  }
}

可以使用「模式匹配」直接获取Map中当前迭代的元组。

bindings map { {
  case (key, value) => "\\\\"" + key + ":" + value + "\\\\""
} }

其中,map的大括号(或小括号)常常被省略。

bindings map {
  case (key, value) => "\\\\"" + key + ":" + value + "\\\\""
}

其中,{ case (key, value) => ??? }表达式是一个整体,包括外面的大括号。此外,该表达式是有类型的,它的类型为PartialFunction[(String, JSON), String]

val f: PartialFunction[(String, JSON), String] = {
  case (key, value) => "\\\\"" + key + ":" + value + "\\\\""
}

也就是说,{ case (key, value) => ??? }是「偏函数」(Partial Function)的一个字面值。

偏函数的定义

一般地,对于函数A => B,它对A类型的所有值都有意义;而PartialFunction[A, B]仅对A类型的部分值有意义。

也就是说,偏函数是一个残缺的函数,它只处理模式匹配成功的值,而对模式匹配失败的值抛出MatchError异常。例如,positive就是一个偏函数,它仅对大于0的整数有意义。

val positive: PartialFunction[Int, Int] = { case x if x > 0 => x }

当传递负数或0值时,将在运行时抛出MatchError异常。

positive(1)   // OK
positive(-1)  // MatchError

剖析偏函数

偏函数是一个一元函数。

trait PartialFunction[-A, +B] extends (A => B) {
  def isDefinedAt(x: A): Boolean
  def applyOrElse[A1 <: A, B1 >: B](x: A1)(default: A1 => B1): B1 =
    if (isDefinedAt(x)) apply(x) else default(x)
}

对于偏函数positive

val positive: PartialFunction[Int, Int] = { case x if x > 0 => x }

为了方便理解,上述偏函数实现类似于如下的匿名函数定义:

val positive = new PartialFunction[Int, Int] { 
  def isDefinedAt(x: A): Boolean = x > 0
  def apply(x: Int): Int = x   
}

追溯MatchError

当调用positive(-1)时,将抛出MatchError异常。追本溯源,它实际调用的是AbstractPartialFunctionapply方法。

AbstractPartialFunction的存在,避免了偏函数的字面值产生大量的匿名类定义。

abstract class AbstractPartialFunction[-T, +R] extends PartialFunction[T, R] {
  def apply(x: T): R = applyOrElse(x, PartialFunction.empty)
}

其中,empty定义在PartialFunction的伴生对象中。

object PartialFunction {
  val empty = new PartialFunction[Any, Nothing] {
    def isDefinedAt(x: Any) = false
    def apply(x: Any) = throw new MatchError(x)
    ...
  }
}

当调用positive(-1)时,AbstractPartialFunctionapply方法被调用,它委托给了PartialFunctionapplyOrElse方法。

因为此时传入的是负数,isDefinedAt返回false,因此最终偏函数的返回值为PartialFunction.empty,而调用empty.apply将抛出MatchError异常。

提升

如果对偏函数进行提升(lift),会将偏函数升级为一个普通的一元函数。例如,对于偏函数positive:

val positive: PartialFunction[Int, Int] = { case x if x > 0 => x } 

通过lift,可以将positive提升为Int => Option[Int]的一元函数。

val positiveOption = positive.lift

positiveOption传入的是正数x时,将返回Some(x),否则返回None

降级

如果对一元函数positiveOption调用Function.unlift,将使得该一元函数降级为一个偏函数。例如:

val positive = Function.unlift(positiveOption)   // positive: PartialFunction[Int, Int]

其中,unlift实现在单键对象Function中,它的函数原型为:

def unlift[T, R](f: T => Option[R]): PartialFunction[T, R] = ???

函数形态

Scala中,可能存在多种风格类似的函数定义(广义的函数定义)。首先,可以以方法(Method)的形式存在。

def lower(s: String): String = s.toLowerCase

特殊地,虽然以方法的形式存在,但它返回了函数类型。因此,也常常成为方法。

def lower: String => String = _.toLowerCase
def lower: Function1[String, String] = _.toLowerCase

另外,也可以使用val持有函数值。

val lower: String => String = _.toLowerCase
val lower: Function1[String, String] = _.toLowerCase
val lower = (s: String) => s.toLowerCase

假如存在一个map的柯里化的函数。

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

它接受上述6种风格的lower定义。

map("HORANCE")(lower)

当然,map也可以直接接受匿名函数(函数字面值)。

map("HORANCE") { _.toUpperCase }

虽然map接受多种不同的lower表示,但三种表示形式存在微妙的差异。

方法与函数

def lower(s: String): String = s.toLowerCase
def lower: String => String = _.toLowerCase

从严格意义上讲,两者都是使用了def定义的「方法」(Method)。但是,后者返回了一个「函数类型」,并具有函数调用的语义。

因此按照习惯,前者常称为「方法」,而后者则常常称为「函数」;前者代表了面向对象的风格,后者代表了函数式的风格。

Scala中,方法与函数是两个不同的概念,并存在微妙的差异,并代表了两种不同的编程范式。但幸运的是,它们的差异性对于99%的用户是无感知的。

方法不是函数

Scala中,方法并不是函数。例如

def lower(s: String): String = s.toLowerCase

lower是一个方法(Method),而方法是没有值的,因此它不能赋予给变量。

val f = lower   // 错误,lower是一个方法,它没有值

可以对方法lower实施η扩展,将其转换为部分应用函数。例如:

val f = lower _  // f: String => String = <function1>

事实上,lower _将生成一个匿名的函数对象。它等价于:

val f = new Function1[String, String] { 
  def apply(s: String): String = this.lower(s)  // 在this对象调用`lower`方法
}

特殊地,当上下文需要一个「函数类型」时,此时编译器可以自动完成「方法」的η扩展。例如,map的第二个参数刚好是一个函数类型。此时,可以直接将lower方法进行传递,编译器自动完成η扩展,将其转换为一个函数对象。

map("HORANCE")(lower)   // map期待一个函数类型,编译器自动完成`η`扩展

它等价于:

map("HORANCE")(lower _)

两阶段调用

def lower: String => String = _.toLowerCase

事实上,此处的lower是一个使用def定义的方法。但是特殊地,它返回了函数类型。当发生如下调用时。

lower("HORANCE")

实际上,它做了两件事情:

  • this对象上调用lower方法,并返回String => String类型的函数对象;
  • 在该函数对象上调用apply方法;

也就是说,上例函数调用等价于:

val f = this.lower  // f: String => String = <function1>
f.apply("HORANCE")

事实上,lower("HORANCE")的调用具有特殊意义。其一,lowerthis对象上调用刚好返回函数类型;其二,FunctionN上的apply是一个特殊的方法,其具有更贴切的函数调用语义。

按照习惯,即使lower是一个方法定义,但也被常常称为函数定义。

def与val

如下两种方式,定义了类似的有名函数,但两者之间存在微妙的差异。

val lower: String => String = _.toLowerCase
def lower: String => String = _.toLowerCase

使用val定义函数

前者使用val的变量直接持有函数值,多次使用lower将返回同一个函数值。也就是说,如下表达式求值为真。

lower eq lower   // true

使用def定义函数

后者使用def定义函数,每次调用lower将得到不同的函数值。也就是说,如下表达式求值为假。

lower eq lower   // false

柯里化

「柯里化(Currying)」是函数式理论的一个重要概念。例如,之前的map就是一个柯里化的方法定义。

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

柯里化类型的自动推演,及其控制结构的抽象等应用场景扮演重要角色。

类型推演

假如,map是一个普通的方法定义,如果没有进行柯里化。

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

当直接传递匿名函数时,如果没有显式地标注参数类型,类型推演将发生错误。

map("HORANCE", s => s.toLowerCase)  // Error

它必须显式地声明匿名函数的入参类型。

map("HORANCE", (s: String) => s.toLowerCase)

如果对map进行柯里化,让其拥有两个柯里化的参数列表。

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

当传递匿名函数时,它便可以自动完成匿名函数参数的类型推演。

map("HORANCE", s => s.toLowerCase)

控制抽象

借助于柯里化,可以实现控制结构的抽象。例如,可以设计一个指令式的until的函数,它将循环调用blok,直至条件cond为真为止(与while相反)。

@annotation.tailrec
def until(cond: => Boolean)(block: => Unit) {
  if (!cond) {
    block
    until(cond)(block)
  }
}

当调用until时,最后一个柯里化参数列表的调用可以使用大括号代替小括号。例如,

var n = 10
var r = 0
until (n == 0) {
  r += n
  n -= 1
}

事实上,它等价于

(0 to 10).sum

前者为指令式的代码风格,后者为函数式的指令风格。

形式化

一般地,具有多个参数列表,并且每个参数列表中有且仅有一个参数的「柯里化」常称为「完全柯里化」。假设存在一个完全柯里化的方法定义,它具有如下的形式:

def f(a1: A1)(a2: A2)...(an: An):R = E

其中,n > 1。它等价于:

def f(a1: A1)(a2: A2)...(an_1: An_1) = an => E

递归这个过程,可以推出最终的等价形式:

def f = (a1 => (a2 => ...(an => E)...))

虽然,后者也是一个使用def定义的方法,但它返回了一个「完全柯里化的函数类型」。按照惯例,后者也常称为「完全柯里化的函数」。

柯里化方法与函数

def concat(s1: String)(s2: String): String = s1 + s2
def concat: String => String => String = s1 => s2 => s1 + s2

按照上述形式化的描述,前者进行2次变换可将其转变为后者,两者功能等价。但是,它们两者之间是存在着微妙的差异,前者是一个方法,它没有值;而后者是一个函数,且拥有函数值。

柯里化方法

首先,对于柯里化的方法,它首先是一个方法(Method)。

def concat(s1: String)(s2: String): String = s1 + s2

因此,它没有值。如下语句将发生编译错误。

val f = concat  // Error

但是,可以应用η扩展将该柯里化的方法转变为柯里化的函数值。

val f = concat _  // f: String => (String => String) = <function1>

如果固定第一个参数的值,必须再实施η扩展,才能得到部分应用函数。

val f = concat("horance") _  // f: String => String = <function1>

柯里化函数

而对于柯里化的函数,它是一个函数(Function)。

def concat: String => String => String = s1 => s2 => s1 + s2

无需η扩展,它便可以直接获取到函数值。

val f = concat    // f: String => (String => String) = <function1>

如果固定第一个参数的值,也不用实施η扩展,便直接可得到的部分应用函数。

val g = concat("horance")  // g: String => String = <function1>

自动扩展

当然,如果存在一个高阶函数twice,它接受String => String => String的函数类型。

def twice[A](x: A)(f: A => A => A): A = f(x)(x)

对于柯里化的方法concat

def concat(s1: String)(s2: String): String = s1 + s2

可以直接传递concattwice,编译器自动完成η扩展。

twice("horance")(concat)

事实上,它等价于:

twice("horance")(concat _)

FunctionN.curried(N >= 2)

一般地,完全柯里化的函数具有如下的类型。

(A1 => (A2 => ...(An => E)...))

可以通过调用FunctionN.curried方法,将一个「非完全柯里化的函数」转化为等价的「完全柯里化的函数」。例如,如果concat是一个普通的方法定义。

def concat(s1: String, s2: String): String = s1 + s2

首先,应用η扩展,将其转变为部分应用函数;此时等到的「部分应用函数」还不是一个完全被柯里化的函数。

val f = concat _    // f: (String, String) => String = <function2>

可以在这个实例对象上通过调用curried将其转变为「完全柯里化」的等价形式。

val g = f.curried  // g: String => (String => String) = <function1>

其中,curried是一个高阶函数,它返回一个完全柯里化的函数。

trait Function2[-T1, -T2, +R] {
  def apply(v1: T1, v2: T2): R
  
  def curried: T1 => T2 => R =
    x1 => x2 => apply(x1, x2)
}

Function.uncurried

可以对g应用Function.uncurried的逆向操作,将其转变为「非柯里化」的函数。

val f = Function.uncurried(g)   // f: (String, String) => String = <function2>

其中,Function.uncurried是这样定义的。

object Function {
  def uncurried[A1, A2, B](f: A1 => A2 => B): (A1, A2) => B =
    (x1, x2) => f(x1)(x2)
}

FunctionN.tupled(N >= 2)

假设存在一个二元组。

val names = ("horance", "liu")

如果要调用如下的函数,完成字符串的连接。

def concat(s1: String, s2: String): String = s1 + s2

它需要依次取出元组中的元素并进行传递。

val fullName = concat(names._1, names._2)   // fullName: String = horanceliu

可以应用Function2.tupled改善设计。首先,应用η扩展将该方法变换为部分应用函数,然后再在该函数对象上调用tupled,并直接传递二元组。

val fullName = (concat _).tupled(names)   // fullName: String = horanceliu

其中,tupled是一个高阶函数,它返回(String, String) => String类型的函数。

trait Function2[-T1, -T2, +R] {
  def apply(v1: T1, v2: T2): R
  
  def tupled: Tuple2[T1, T2] => R = {
    case Tuple2(x1, x2) => apply(x1, x2)
  }
}

Function.untupled

可以对g应用Function.untupled的逆向操作,将接受元组类型参数的函数转变为接受参数列表的函数。

val g = (concat _).tupled     // g: ((String, String)) => String = <function1>
val f = Function.untupled(g)  // f: (String, String) => String = <function2>

其中,Function.untupled是这样定义的。

object Function {
  def untupled[A1, A2, B](f: (A1, A2) => B): (A1, A2) => B = 
    (x1, x2) => f((x1, x2))
}

实战ScalaHamcrest

对于任意的类型AA => Boolean常常称为「谓词」;如果该谓词用于匹配类型A的某个值,也常常称该谓词为「匹配器」。

ScalaHamcrest是一个使用函数式设计思维实现的Matcher集合库。通过实战ScalaHamcrest,以便加深理解Scala函数的理论基础。

原子匹配器

可以定义特定的原子匹配器。例如:

def always: Any => Boolean = _ => true
def never:  Any => Boolean = _ => false

也可以定义equalTo的原子匹配器,用于比较对象间的相等性。

def equalTo[T](expected: T): T => Boolean =
  expected == _

equalTo类似,可以定义原子匹配器same,用于比较对象间的一致性。

def same[T <: AnyRef](t: T): T => Boolean = t eq _

其中,T <: AnyRef类型T进行界定,排除AnyVal的子类误操作same。类似于类型上界,也可以使用其他的类型界定形式;例如,可以定义instanceOf,对类型T进行上下文界定,用于匹配某个实例的类型。

def instanceOf[T : ClassTag]: Any => Boolean = x =>
  x match {
    case _: T => true
    case _    => false
  }

有时候,基于既有的原子可以很方便地构造出新的原子。

def nil   = equalTo[AnyRef](null)
def empty = equalTo("")

组合匹配器

也可以将各个原子或者组合器进行组装,形成威力更为强大的组合器。

def allOf[T](matchers: (T => Boolean)*): T => Boolean =
  actual => matchers.forall(_(actual))

def anyOf[T](matchers: (T => Boolean)*): T => Boolean =
  actual => matchers.exists(_(actual))

特殊地,基于anyof,可以构造很多特定的匹配器。

def blank: String => Boolean = """\\\\s*""".r.pattern.matcher(_).matches

def emptyOrNil = anyOf(nil, equalTo(""))
def blankOrNil = anyOf(nil, blank)

修饰匹配器

修饰也是一种特殊的组合行为,用于完成既有功能的增强和补充。

def not[T](matcher: T => Boolean): T => Boolean =
  !matcher(_)
  
def is[T](matcher: T => Boolean): T => Boolean =
  matcher

其中,not, is是两个普遍的修饰器,可以修饰任意的匹配器。此外,可以通过定义语法糖,提升用户感受。例如,可以使用not替换not(equalTo)is替代is(equalTo),不仅减轻用户的负担,而且还能提高表达力。

def not[T](expected: T): T => Boolean = not(equalTo(expected))
def is[T](expected: T):  T => Boolean = is(equalTo(expected))

测试用例

至此,还不知道ScalaHamcrest如何使用呢?可以定义一个实用方法assertThat

def assertThat[A](actual: A, matcher: A => Boolean) =
  assert(matcher(actual))

其中,assert定义于Predef之中。例如存在如下一个测试用例。

assertThat(2, allOf(always, instanceOf[Int], is(2), equalTo(2)))

增强特性

遗留一个很有意思的问题,能否使用&&直接连接多个匹配器形成调用链,替代allOf匹配器呢?

assertThat("horance", equalTo("horance") && ignoringCase(equalTo)("HORANCE"))

可以定义个一个implicit Matcher,并添加了&&, ||, !的基本操作,用于模拟谓词的基本功能。

implicit class Matcher[-A](pred: A => Boolean) extends (A => Boolean) {
  self =>

  def &&[A1 <: A](that: A1 => Boolean): A1 => Boolean =
    x => self(x) && that(x)

  def ||[A1 <: A](that: A1 => Boolean): A1 => Boolean =
    x => self(x) || that(x)

  def unary_![A1 <: A]: A1 => Boolean =
    !self(_)

  def apply(x: A): Boolean = pred(x)
}

总结

本文重点叙述了Scala函数式编程的基础,重点讨论了几个容易混淆的概念。包括「方法」与「函数」,「偏函数」与「部分应用函数」,「柯里化」,「高阶函数」等,并通过实战ScalaHamcrest,深入理解Scala函数的基本概念。

接下来,将重点讨论函数式编程的应用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容