Skip to content

Instantly share code, notes, and snippets.

@SeijiEmery
Last active August 29, 2015 14:06
Show Gist options
  • Save SeijiEmery/bcf55d433c58a1eb7433 to your computer and use it in GitHub Desktop.
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…
//
// 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