The (well-intentioned) introduction of classes in ES6 is one example.
The lack of a standard interface for identifying a value's prototype is another.
ES6 (quietly) addressed this latter problem with introduction of
Which means we can reliably write code that knows about prototypes.
A Deeper Understanding Of Prototypes
First, let's define a simple wrapper around
prototype = (value) -> if value? then Object.getPrototypeOf value
Next, let's define a function that allows us to check the prototype.
isPrototype = curry (p, value) -> p? && p == prototype value
We can now check the type of a value by comparing it to the
prototype property of a given type.
That's the property we copy when we want to create a new instance of that type.
isArray = isPrototype Array.prototype assert isArray [1,2,3] assert !isArray 7
We take advantage of the fact that we can curry
isPrototype to define
isArray a simply a curried version of
So far, so good. But we can still clean this up a bit.
isType = curry (type, value) -> isPrototype type?.prototype, value
Now we can easily define type-checking helpers. We take advantage of currying again, only now we can just pass in the type, instead of the prototype.
isArray = isType Array assert isArray [1,2,3] assert !isArray 7
However, if we want to answer the question whether a given value is a bank account, we can no longer simply rely on the value's prototype. Because its prototype might be our prototype checking account. Which is not the same as our prototype bank account. What we need to do is check the entire chain of prototypes to see if we eventually reach the bank account prototype.
It first checks the value directly for the given property.
Then it checks the value's prototype.
Then it checks the prototype of that prototype.
And so on, until it reaches a value with no prototype (that is, the value of the prototype is
BankAccount = prototype: number: 0 CheckingAccount = prototype: Object.create BankAccount.prototype myAccount = Object.create CheckingAccount.prototype assert myAccount.number == 0
Even though we never defined a
number property on
myAccount or on the
CheckingAccount prototype from which we created it, it's still there.
Of course, that's because we defined it on the prototype for
BankAccount, which we used to define the prototype for
Of course, we want our helper functions to be able to take this prototype chain into account.
The problem with
instanceof is that, for historical reasons, it doesn't work as reliably as we'd like.
# huh? assert !(7 instanceof Number)
So we need to define a function to allow us to check the prototype chain directly. This is analogous to our function that checks the prototype.
isTransitivePrototype = curry (p, value) -> p? && (p == (q = prototype value) || (q && isTransitivePrototype p, q))
We check to see if the given prototype is in fact that prototype for the given value.
If not, we call
isTransitivePrototype recursively with the value's prototype, effectively following the prototype chain.
prototype property, we can define a type-centric variant of the same function.
This is the prototype-chain tracing analog to
isKind = curry (type, value) -> isTransitivePrototype type?.prototype, value
We can now define type-checking helpers based on the prototype chain.
isBankAccount = isKind BankAccount assert isBankAccount myAccount assert !isBankAccount, number: 1
isKind we can easily define type-checking functions.
But what about defining new types?
Can we make that a little easier?
Let's define two helper functions for defining new types and creating instances of those types.
Type = create: (type) -> if type? then Object.create type.prototype define: (parent = Object) -> prototype: Type.create parent
Now we can define our bank account and checking account types more succinctly.
BankAccount = Type.define number: 0 CheckingAccount = Type.define BankAccount myCheckingAccount = Type.create CheckingAccount assert isType CheckingAccount, myCheckingAccount assert isKind BankAccount, myCheckingAccount
You've probably noticed by now that we're not using constructor functions or the
new operator anywhere.
One reason we're avoiding this is that we can (accidentally) call constructor functions like ordinary functions.
(That is, without using the
This can lead to unexpected bugs which are difficult to find.
We can prevent these by checking the value of
this in the constructor function, but that's still error-prone and awkward.
are other reasons
to avoid using
Instead, we construct objects using our
But what if we want to initialize the value?
For example, what if we want to assign a unique account number to each account upon creation.
Simple: just add a
create method to your type object.
CheckingAccount.generateAccountNumber = do (n = 0) -> -> n++ CheckingAccount.create = -> account = Type.create CheckingAccount account.number = CheckingAccount.generateAccountNumber() account firstAccount = CheckingAccount.create() secondAccount = CheckingAccount.create() assert firstAccount.number != secondAccount.number
This works fine for a single type.
But what if we want to ensure unique account numbers across all
If we were using classes, we'd reference the super class constructor, which would take care of setting the account number.
But with prototype chains, there's no “super” class.
So how can we allow
CheckingAccount to take advantage of common initialization details implemented by all bank accounts?
The most general solution is to explicitly call the desired initialization function.
That is, just call something along the lines of
While this isn't as succinct or expressive as
super, there's nothing wrong with being explicit.
BankAccount = Type.define() BankAccount.generateAccountNumber = do (n = 0) -> -> n++ BankAccount.initialize = (account) -> account.number = BankAccount.generateAccountNumber() account CheckingAccount = Type.define BankAccount CheckingAccount.create = -> BankAccount.initialize Type.create CheckingAccount
Arguably, this is actually easier to reason about than