Last active
August 29, 2015 14:06
-
-
Save SeijiEmery/bcf55d433c58a1eb7433 to your computer and use it in GitHub Desktop.
Javascript utility library – implements a pythonic type system, a class creation function, some utility functions like map, reduce, join (nevermind that they're already part of Array.*), and a hacked together form of function overloading. Also contains my some of my notes on the language, which is worth a read if you're interested in language-de…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// type.js | |
// Created by Seiji Emery 5/5/2014 | |
// | |
// Defines (among other things) a class creation facility, a simple type-system | |
// with strong reflective capabilities, and a mechanism for strongly-typed | |
// function overloading (using said type system). | |
// | |
// Disclaimer: None of these are very fast (overloading is implemented by stringifying | |
// types into a a lookup string that pulls stuff out of a dictionary of function | |
// overloads), and there are undoubtedly much faster and more efficient implementations | |
// of these concepts in existing javascript libraries. | |
// | |
// This is primarily intended as a learning exercise in how these concepts can be | |
// implemented in javascript, and and provides a very interesting look at how the language | |
// works. The code is very simple (albeit not very well documented), and I hope someone will | |
// find it useful :) | |
// | |
/* | |
// A few notes/first impressions on the language | |
// (As someone coming python with a so-far only half-baked knowldege of js): | |
// - The language is very similar to python, only with fewer built-in features and | |
// basically missing a user-defined type system. | |
// - Like python, the core language basically consists of just two things: functions and | |
// dictionaries/objects (plus built-in types like strings and numbers, and built-in | |
// operations like +, -, repr, etc). A python class statement is just syntax sugar, so | |
// the fact that javascript lacks this is inconsequential. | |
// - Python gives you many metaprogramming hooks to add features and change the semantics | |
// of the language (albeit at a performance penalty); javascript does not, so you're stuck | |
// with sometimes non-optimal syntax and no operator overloading means everything not builtin | |
// has to be expressed as a function, which doesn't always work well. | |
// - Prototypes are interesting. They're a pseudo-type system actually quite similar to | |
// python types that enable inheiritance and class methods by having one object stand | |
// in as another's 'class'/prototype. The mechanism behind this is simple: if object | |
// A has prototype B, then attribute lookups are forwarded to B if a requested var | |
// can't be found in A (undefined/null). And if B doesn't have it it's forwarded to B's | |
// prototype, etc., until either a value is found or we reach the top of the type hierarchy | |
// This is similar to how python attribute/method lookups work (albeit with a more limited | |
// hierarchy), and you can implement the exact same thing in python by extending the dict | |
// class with metaprogramming hooks. | |
// - The 'this' keyword is just syntax sugar and basically means that javascript always | |
// passes a hidden argument that refers to the calling context/bound object. This is | |
// basically equivalent to python's self/cls first argument to object/class methods, | |
// except that it is implicit and always passed (even where it doesn't make much sense | |
// to do so - ie. with standalone, non-object related functions) | |
// - The 'new' keyword is a little wierd and seems to be a Java-ism thrown in to try to make | |
// certain programmers feel at ease with familiar syntax (but NOT semantics) - just like | |
// everything else about the language. Not very useful (language-wise); basically just | |
// syntax sugar to create a new dict/object, pass it to a constructor function, and | |
// return the result. | |
// - A builtin clone/copy function would be much more useful imo | |
// - Javascript just *barely* has a type system. | |
// - Only six types: string, object, number, boolean, function, and undefined. | |
// - typeof() and exceptions return/use strings | |
// - No float, int, unsigned, or char types | |
// - No builtin lists or arrays (objects/dicts stand in for that instead - seems | |
// incredibly inefficient, but whatever) | |
// - *No* user defined types. Anything you create is an object (how 'convenient') | |
// - Prevents overloading (also a problem with python), and means there's no way to | |
// querry information about objects other than what attributes they have | |
// - No type safety (obviously). | |
// - Can't limit parameter types without inefficient if/else statements (admittedly | |
// also a problem in python, though at least there you have an actual type system | |
// with reflection and metaprogramming) | |
// - Parameters are completely unconstrained - any function can, and will take *any* | |
// number of parameters, regardless of what the parameter list says. | |
// - Actually, parameters seem to be be basically just syntax sugar into the arguments | |
// array... | |
// - Syntax errors can be hidden inside functions and won't be found until they're actually evaluated | |
// - Another problem with python... | |
// - Exceptions are rarely used | |
// - Python's approach to undefined behavior is to throw an exception, which can be | |
// handled appropriately and flexibly without compromising program integrity | |
// - Javascript's approach to undefined behavior is to silently convert it to | |
// 'correct' behavior, by leaving all operations technically defined (eg. add a number | |
// to a random composite object), but which may result in bizarre and sometimes | |
// nonsensical and probably inefficient behavior. | |
// - If in doubt, it's probably a string | |
// - Javascript's default behavior when faced with two mutually incompatible | |
// types and the '+' operator seems to be to stringify the two values and | |
// then concatate them | |
// - '-' seems to default to NaN, because... idk, maybe the language devs | |
// ran out of ideas and thought "" - {} => NaN sounded good on paper? | |
// - Compare this to Haskell, which inherently *disallows* undefined behavoir and catches | |
// virtually all errors during compilation (strong static type systems ftw) | |
// | |
// - I have no idea how this language is optimized. | |
// - Like python, everything seems to be either a dict (implemented in python as hashtables) or | |
// strings, and neither of these maps well to assembly/low level bytecode. Javascript seems | |
// to be constantly translating stuff to/from strings, and has to have a complicated memory/ | |
// object model under the hood. From what I've seen of the language, I'd think it *should* be | |
// slower than python, so it's speed (only several times slower than native with modern jit-ing | |
// compilers), is quite frankly mind boggling. | |
// | |
// - Useful things to know | |
// - The best way to implement 'classic' oop ( <- unintentional pun), is to stick class methods | |
// and members/attributes in an object, then assign to the .prototype of your constructor | |
// function (which may in fact be a member of the class). If Foo is the name of the constructor | |
// method, use new Foo() to create an object instance. | |
// - Better yet is just to write a makeClass() function which takes a class object/dict and | |
// does this all for you (see makeClass()). | |
// - 'arguments' gives you direct access to a function's parameter list | |
// - If a parameter is not passed its value will be 'undefined' (actually a valid value type) | |
// - If you access a variable you have not yet used, its value will be undefined (unless you try | |
// to do something with it and it gives you a reference error...) | |
// - 'foo = undefined' is equivalent to 'del foo' in python. | |
// - <function>.apply(this, args) is extremely useful and very important for calling functions | |
// and forwarding arguments | |
// - protip: If you want to forward a function's parameters to another function (eg foo), | |
// calling foo(arguments) will send foo your arguments as a list in *one* parameter. | |
// For proper forwarding (ie. foo gets the same arguments list that you do), call | |
// foo.apply(this, arguments) (or foo.apply(bar, arguments) if you want foo to modify | |
// the object bar instead of this (foo's this points to bar instead of your this)) | |
// - Javascript closures are awesome | |
// - One of the few things I unambiguously love about this language. | |
// - toString() is the implicit string converter function, so define this for any function | |
// that may need to be string-ified. | |
// - String conversion can be done via concating a string with any other object. | |
// eg. '' + foo | |
*/ | |
// | |
// Useful defined methods/functions and objects | |
// | |
// makeClass(typeName, classDict) | |
// Takes a dict of class members/methods and defines/returns a create() method that sets | |
// the constructor's prototype, creates/returns an instance, and calls the class constructor. | |
// Expects the constructor to be named 'construct' | |
// typeName is _required_ and should match the class name (so Foo = makeClass('Foo', {...})) | |
// | |
// getType (values...) | |
// Sort-of-extension to builtin typeof(). Allows objects to define their own type by using | |
// the .type field, which is very useful, and it's recommended to use getType as a general | |
// replacement for typeof(). | |
// typeof({ .type = "foo", ... }) => "object" | |
// getType({ .type = "foo", ... }) => "foo" | |
// getType can also take more than one value as a parameter, in which case it will return | |
// the value types as a comma separated list. This is useful in certain situations and is | |
// the foundation of the overloading system. | |
// | |
// Type | |
// Global base type class; stores type info for builtin types (number, string, function, etc), | |
// and user-defined types (automatically registered by makeClass). | |
// | |
// builtin types | |
// Type.number, Type.string, Type.function, Type.boolean, Type.object, Type.undefined | |
// Type.number == typeof(1) == getType(1) | |
// Type.function == typeof(function (foo, bar) {}) == getType(function (foo, bar) {}) | |
// etc. | |
// | |
// Type.join (types...) | |
// Joins several types. Types are expected to be strings or string-able (objects with toString | |
// defined), and are expected to represent actual types (either builtin or user defined). | |
// Designed to interoperate with getType: | |
// getType(1, 2, "foo", Bar(baz, 10)) == Type.join(Type.number, Type.number Type.string, Type.Bar) | |
// == Type.join(number, number, string, Bar.type) | |
// | |
// makeOverloaded (overloads...) | |
// Creates an overloaded function; takes 1 or more overload(s) (see overload) | |
// | |
// overload (paramType, fcn) | |
// Creates a type/fcn pair usable by makeOverloaded. paramType should be created via ftype(types...), | |
// (alias for Type.join), which takes a variable number of parameter types matchable to the function | |
// | |
// print (value) | |
// Useful wrapper around console.log that stringifies its value and prints it. | |
// | |
// Utility functions... | |
// Simple, fairly useful set operation (a not in S) | |
var notInList = function (elem, list) { | |
for (var i = 0; i < list.length; ++i) { | |
if (elem == list[i]) | |
return false; | |
} | |
return true; | |
} | |
// Should probably use builtin, but whatever... | |
var join = function (sep, elems) { | |
if (elems.length == 0) | |
return ""; | |
var joined = '' + elems[0]; | |
for (var i = 1; i < elems.length; ++i) { | |
joined += sep + elems[i]; | |
} | |
return joined; | |
} | |
var map = function (list, fcn) { | |
for (var i = 0; i < list.length; ++i) { | |
list[i] = fcn(list[i]); | |
} | |
return list; | |
} | |
var reduce = function (pred, initial, list) { | |
var result = initial; | |
for (var i = 0; i < list.length; ++i) { | |
result = pred(result, list[i]); | |
} | |
return result; | |
} | |
/* | |
// Alt implementation: wrapper around Array.map | |
var map = function (pred, elems) { | |
return Array.apply(this, elems).map(pred) | |
} | |
*/ | |
var print = function (val) { | |
console.log('' + val) | |
} | |
// Create typesystem | |
var Type = { | |
type: 'type', | |
join: function (/* types... */) { | |
return join(", ", arguments); | |
}, | |
__addType: function (typeName, classDict) { | |
if (notInList(typeName, ['type', 'join', '__addType', '__builtin']) && | |
notInList(typeName, Type.__builtin)) | |
{ | |
var typeInfo = { | |
class: classDict, | |
type: 'type', | |
toString: function () { return typeName; } | |
} | |
classDict.type = typeInfo; | |
// Register into global typesystem | |
Type[typeName] = typeInfo; | |
} else { | |
if (notInList(typeName, Type.__builtin) && typeName != 'type') | |
print("Could not create type '" + typeName + "' due to conflicts with Type internals. "); | |
else | |
print("Could not create type ('" + typeName + "' is a builtin type and may not be overridden)."); | |
} | |
}, | |
__builtin: ['number', 'string', 'boolean', 'object', 'function', 'undefined'] | |
} | |
var getType = function (/* values... */) { | |
return join(", ", map(arguments, function (val) { | |
if (typeof(val) == 'object') { // Can have user-defined type | |
if (val.type != undefined) { | |
return val.type; | |
} | |
} | |
return typeof(val); | |
})); | |
} | |
// Add builtin types: | |
Type.__addBuiltin = function (val) { | |
var typeName = typeof(val) | |
Type[typeName] = { | |
type: 'builtin', | |
toString: function () { return typeName; } | |
} | |
} | |
Type.__addBuiltin(1); | |
Type.__addBuiltin(''); | |
Type.__addBuiltin(true); | |
Type.__addBuiltin({}); | |
Type.__addBuiltin(function (){}); | |
Type.__addBuiltin(undefined); | |
Type.__addBuiltin = undefined; | |
// Create aliases for commonly used types that don't conflict with keywords: | |
var number = Type.number; | |
var string = Type.string; | |
var bool = Type.boolean; | |
// Constructs a class from a (hopefully) unique typeName | |
var makeClass = function (typeName, classDict) { | |
Type.__addType(typeName, classDict) | |
var _baseConstructor = function () {}; | |
_baseConstructor.prototype = classDict; | |
// Constructor | |
classDict.create = function () { | |
var instance = new _baseConstructor(); | |
instance.construct.apply(instance, arguments); | |
if (instance.__cancelConstruct) { | |
// Special way to allow a constructor to cancel object creation (returning undefined instead). | |
// The alternative would be to require constructors to always return 'this' (which enables | |
// them to return something else (like undefined) instead, if desired). | |
return undefined; | |
} | |
return instance; | |
} | |
return classDict.create; | |
} | |
// Add overloading capabilities: | |
makeOverloaded = function (/* overloads... */) { | |
var overloads = {} | |
var defaultOverload = undefined | |
for (var i = 0; i < arguments.length; ++i) { | |
var ts = arguments[i].typesig; | |
var fcn = arguments[i].fcn; | |
if (typeof(ts) != 'string' || typeof(fcn) != 'function') { | |
// print("Discarding " + ts + fcn); | |
continue; | |
} | |
if (ts === 'default') { | |
// print("Creating default overload"); | |
defaultOverload = fcn; | |
} else { | |
// print("Creating overload for " + ts) | |
overloads[ts] = fcn; | |
} | |
} | |
return function (/* args... */) { | |
var overload = overloads[getType.apply(this, arguments)] | |
if (overload !== undefined) { | |
// print("Found overload: " + getType.apply(this, arguments)); | |
return overload.apply(this, arguments) | |
} | |
if (defaultOverload !== undefined) { | |
// print("Found default overload"); | |
return defaultOverload.apply(this, arguments) | |
} | |
// print("Could not find overload for " + getType.apply(this, arguments)); | |
return undefined; | |
} | |
} | |
overload = function (typeSignature, fcn) { | |
return { | |
typesig: typeSignature, | |
fcn: fcn | |
} | |
} | |
var ftype = Type.join; | |
// Simple use case: (no overloading) | |
Adder = makeClass('Adder', { | |
construct: function (name) { | |
this.name = name; | |
this.count = 0; | |
}, | |
add: function () { | |
this.count++; | |
return this; | |
}, | |
toString: function () { | |
return '' + this.name + ' counter (count = ' + this.count + ')'; | |
} | |
}) | |
foo = Adder('bean') | |
print(foo) | |
print(foo.add().add()) | |
// Create instance using type reflection (makeClass automatically registers Adder as an 'Adder' type) | |
bar = Type.Adder.class.create('meta') | |
print(bar.add()) | |
// And we can even do this... (true type reflection) | |
baz = foo.type.class.create('reflective') | |
print(baz.add().add().add().add()) | |
// Wonder why we store every created class's type in the global Type dict? | |
// It's so we can do stuff like this: | |
var fooType = getType(foo); | |
print('fooType is a ' + getType(fooType)); | |
newFoo = Type[fooType].class.create('clone of ' + foo.name); newFoo.count = foo.count; | |
print(newFoo.add()); | |
print(foo) | |
// Simple overload example | |
whatAmI = makeOverloaded( | |
overload(ftype(number), | |
function (num) { | |
return "I'm a number!" | |
}), | |
overload(ftype(string), | |
function (str) { | |
return "I'm a string! " + str; | |
}), | |
overload(ftype(number, number, number), | |
function (a, b, c) { | |
return "I'm three numbers: " + a + ", " + b + ", " + c; | |
}), | |
overload(ftype(Type.Adder), | |
function (adder) { | |
return "I'm an adder, and I've counted to " + adder.count; | |
}), | |
overload('default', | |
function (val) { | |
if (arguments.length == 0 || val === undefined) | |
return "I'm undefined!"; | |
else if (arguments.length == 1) | |
return "I'm a " + getType(val) + "!"; | |
else | |
return "I am multiple things." | |
}) | |
); | |
x = undefined; | |
print(whatAmI(42)); | |
print(whatAmI("foo")); | |
print(whatAmI(10, 12, 13)); | |
print(whatAmI(10, 12, 13, 15)); // Not covered by any of the type cases, so calls default | |
print(whatAmI(true)); | |
print(whatAmI(Adder().add().add().add())); | |
print(whatAmI(x)); | |
// makeOverload can also be used to create overloaded class methods, and even constructors. | |
// In this case, we use overloading to create a simple, strongly typed Fraction class that | |
// conforms to the following rules: | |
// - Fraction may be initialized with one or two values (n, d) with d optional, but both must be | |
// numbers (not strings, functions, booleans, or composite objects) | |
// - add() method only operates on numbers or other fractions | |
// - other methods (mixed, which returns the fraction represented as a midex number, and toString) | |
// are normal functions. | |
Fraction = makeClass('Fraction', { | |
construct: makeOverloaded( | |
overload('Fraction', | |
function (other) { | |
this.n = other.n; | |
this.d = other.d; | |
}), | |
overload(ftype(number), | |
function (n) { | |
this.n = n; | |
this.d = 1; | |
}), | |
overload(ftype(number, number), | |
function (n, d) { | |
this.n = n; | |
this.d = d; | |
}), | |
overload('default', | |
function () { | |
this.__cancelConstruct = true; | |
}) | |
), | |
toString: function () { | |
if (d == 1) | |
return 'Fraction ' + this.n; | |
return 'Fraction ' + this.n + ' / ' + this.d; | |
}, | |
add: makeOverloaded( | |
overload(ftype(number), | |
function (n) { | |
this.n += n * this.d; | |
return this; | |
}), | |
overload(ftype('Fraction'), | |
function (other) { | |
this.n = this.n * other.d + other.n * this.d; | |
this.d *= other.d; | |
return this; | |
}) | |
), | |
mixed: function () { | |
if (this.n % this.d == 0) | |
return '' + this.n / this.d; | |
return '' + (this.n / this.d) + ' + ' + (this.n % this.d) + '/' + this.d; | |
} | |
}) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment