运行时类型检查对Duck typing是必须的?

March 5, 2014

听说过duck typing吧,如果没有就先Google一下吧。

问题

假设下面不是ruby代码,duck的类型是什么?(代码来自《冒号课堂》)

def shoutOrSwim(duck, flag)
    flag ? duck.shout : duck.swim
end

你要先想想。

运行时类型检查?

我现在认为运行时类型检查对duck typing来说不是必须的(几个小时前我还在动摇)。一般情况下,我认为duck必须是个同时支持shoutswim的对象。

但是有人就说:“在ruby里,只要在运行时程序员自己保证flag为真时duckshoutflag为假的时候duckswim就可以了,所以duck不需要同时有两个方法,这在静态语言是做不到的”。

这里我们不讨论method_missing这些“故意”通过调用不存在的方法而触发的东西,静态语言可以通过依赖类型实现shoutOrSwim,而且duck不须同时有shoutswim。这里我用Haskell模拟一下依赖类型就可以实现1

{-# LANGUAGE TypeFamilies, DataKinds, ConstraintKinds, GADTs #-}
import GHC.Exts (Constraint)

-- Ad-hoc polymorphism

class ICanShout a where
  shout :: a -> String

class ICanSwim a where
  swim :: a -> String

data Man = Man
instance ICanShout Man where
  shout _ = "Hahaha"

data Fish = Fish
instance ICanSwim Fish where
  swim _ = "Swimming"


-- Dependent type (Simulating via singleton)

type family Select (flag :: Bool) :: * -> Constraint
type instance Select True  = ICanShout
type instance Select False = ICanSwim

data SBool :: Bool -> * where
    STrue :: SBool True
    SFalse :: SBool False

shoutOrSwim :: (Select b) a => SBool b -> a -> String
shoutOrSwim STrue duck = shout duck
shoutOrSwim SFalse duck = swim duck

因为Haskell的记录类型不是结构化类型的,所以代码的第一部分通过type class实现重载,但这不是重点。在第二部分,我们通过用type family和GADT模拟依赖类型,描述了BoolICanShoutICanSwim这两个约束之间的关系,所以在shoutOrSwim的两个分支,duck的类型约束是不同的。这也可以理解为shoutOrSwim根据第一个参数返回不同类型的函数:

ghci> :t shoutOrSwim SFalse
shoutOrSwim SFalse :: ICanSwim a => a -> String

ghci> :t shoutOrSwim STrue
shoutOrSwim STrue :: ICanShout a => a -> String

在一个直接支持的依赖类型和结构化类型的语言,实现shoutOrSwim会更简洁,但是思路是一样的。所以我认为结构化类型(或类似type class的重载方法)加上依赖类型就能覆盖大部分Duck typing的用法,特别是不做shoutOrSwim这种坑爹的事,根本就不需要依赖类型。

什么是Duck typing?

Duck typing没有准确的定义,但是我认为这句话概括了它的本质:

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

也就是说,我们更关心一个对象的行为,而不关心它到底是什么。一个对象的行为既可以通过结构化类型来隐式表达,又可以通过接口、type class显式表达,这些都不需要运行时类型检查。我甚至可以说Duck typing就是隐式的面向接口编程,和动、静态根本就关系。


  1. 其实我偷懒了,因为shoutOrSwim里用是SBool,其实还需要一个把Bool转成SBool的过程,这是一个从动到静的过程,完成之后我们就进入静态的世界。