Skip to content

Instantly share code, notes, and snippets.

@bsansouci
Last active November 27, 2016 17:36
Show Gist options
  • Save bsansouci/ca4dee603ed2bd8ed0a8c4e939f9b2d2 to your computer and use it in GitHub Desktop.
Save bsansouci/ca4dee603ed2bd8ed0a8c4e939f9b2d2 to your computer and use it in GitHub Desktop.
Object syntax proposal.
/**
* Object syntax proposal.
*
* Look at Jordan's named arguments proposal to understand the named args
* syntax used here:
* https://gist.github.com/jordwalke/119fb44b95fdf7d703041f8ac54b5b4e
*
* This is building on top of the ideas here:
* https://github.com/facebook/reason/pull/767
*
* The `pub`/`pri` had been added as a way to distinguish between an open curly
* brace for records and an open curly brace for objects, as the latter would
* be immediately followed by either `pub` or `pri`.
* We know we need to distinguish between public and private methods in objects,
* so one of the two `pub` or `pri` needs to be explicit but the other could be
* implicit, given that we have another mean of determining whether the open
* curly brace is an object.
* It seems that `{.` for closed objects and `{..'a` for open objects does help
* disambiguate.
*
* I went with the idea of using `->` instead of Jordan's idea of using
* `obj.methodName:` just to explore an alternative. I haven't been able to come
* up with a reason why `->` is simpler or better than Jordan's idea.
*
* A new syntax for getter methods
* `let obj = {.. field: 0}` which effectively represent something that you
* "call" by simply doing `obj->field`. I prefer this over
* `let obj = {pub field => 0}` for two reasons:
* 1) it's the same as record syntax, so it's really familiar
* 2) it makes getters (and thefore data), the simplest thing to reach for when
* using objects. Given that a big use case of objects it the underlaying
* row polymorphism, I'd really love to be able to use objects as simple bags
* of data in the simplest way possible.
*
* See below for explanation by example.
*
* I'd love some feedback about any ambiguity that could arise.
*/
/* Closed object type - one dot: these are very rare. */
type characterT1 = {.
x: int,
setX: int => unit,
y: int,
setY: int => unit,
characterType: string,
/* We need the extra unit arg at the end to make it clear if we want to curry
or call the function without that last optional argument. */
doSomething: a:int => b:int => c:string? => unit => int,
};
/* Row polymorphic object type - two dots. */
type characterT2 = {..'a
/* Those look exactly like record fields but they're object methods, see
https://realworldocaml.org/v1/en/html/objects.html */
x: int,
setX: int => unit,
y: int,
setY: int => unit,
characterType: string,
doSomething: a:int => b:int => c:string? => unit => int,
};
/**
* Record type - no dots.
* This is very similar to the object type, except instead of setters we use
* mutable fields to achieve the same behavior.
*/
type characterT3 = {
mutable x: int,
mutable y: int,
characterType: string,
doSomething: a:int => b:int => c:string? => unit => int,
};
/* Faux-polymorphic records (one day?) - three dots */
type characterT3 = {...'a
mutable x: int,
mutable y: int,
characterType: string,
doSomething: a:int => b:int => c:string? => unit => int,
};
/**
* Instance of a character - looks very close to the type definition AND very
* close to record declaration. Notice the `..` right after the `{`.
*
* Objects contain 2 concepts: data fields (called "values" in ocaml
* `val mutable x = 0`) and methods.
* The data fields (which are simply called "values" in OCaml) don't necessarily
* have `_` in front of them, those are here for readability purposes. The name
* of the value is preceded by `val` and, if mutable, `mutable`.
* Methods are either simple 1-1 mappings to values (`x: _x`), which
* under-the-hood are still messages passed to the object like
* `myCharacterObject->x`. Or they're functions that take arguments. See below.
*
* Not sure about `mutable` keyword here, though I think this is how mutable
* record fields are declared. This kind of simple mapping (between a private
* and mutable data field `_x` and a public accessor `x`) could be generated. If
* I'm not mistaken, objective-c does something very similar with ivars.
*/
/**
* Things that we need to express when checking for ambiguities:
* Data fields (mutable and immutable)
* public methods
* private methods
* getters (public methods that don't take any arguments)
* records vs object vs block expression when hitting `{`
*/
let myCharacterObject : characterT2 = {..
/* x coord related things */
val mutable _x: 0,
x: _x,
setX: fun x => _x = x,
/** y coord related things */
val mutable _y: 0,
y: _y,
setY: y => _y = y,
/** `characterType` doesn't expose a setter, so let's not make it mutable. */
val _characterType: "unknown character",
characterType: _characterType,
/** This is just to demonstrate private calls. */
doSomething: fun a:aa :b :c="" () => {
doWorrisomeMutation x:aa y:b;
/* Calls the getter `x` using a magically defined `self`, not sure about this. */
let _ = self->x;
},
/**
* What if we had a little shorthand for functions, like functions in classes
* in ES6? Just a thought experiment. I'm not a huge fan of this as we're
* breaking away from the idea of a mappin from key to a value (or a message
* to a function). Also this seems to be uselessly different from record function syntax.
*/
private doWorrisomeMutation :x :y => {
/* This feels like mutable record fields more than refs...
Not sure which one's better (ref uses `:=`). */
_x = x;
_y = y;
},
};
/* Can't have a private field in a record and no need for internal data fields. */
let myCharacterRecord : characterT3 = {
x: 0,
y: 0,
characterType: "unknown character",
doSomething: fun a:aa :b :c="" () => {
print_endline @@
"a: " ^ (string_of_int aa) ^ ", b: " ^ (string_of_int b) ^ ", c: " ^ c ^ ", characterType: " ^ characterType;
},
};
/* Only difference here is `->` vs `.` */
let myRes = myCharacterObject->x + myCharacterObject->y;
let myRes = myCharacterRecord.x + myCharacterRecord.y;
/* Passing named arguments and purposely omitting the last one `c` */
let myRes = myCharacterObject->doSomething a:10 b:1 ();
let myRes = myCharacterRecord.doSomething a:10 b:1 ();
let myRes = myCharacterObject->doSomething a:10 b:1 c:"woop" ();
let myRes = myCharacterObject.doSomething a:10 b:1 c:"woop" ();
/* Type checker error: method is private. */
let myRes = myCharacterObject->doWorrisomeMutation x:100 y:100;
/* Potentially problematic: record destructuring VS object inline type annotating. */
let {x, y} = myCharacterRecord;
let c:{..'a, x: int, y: int} = myCharacterObject;
/**
* Punned objects named arguments with type annotations.
* This exhibits the row polymorphism of objects.
* (ps: I really wish we could destructure objects, see bottom)
*/
let distanceBetweenObjects
:a:{..'a, x: int, y: int}
:b:{..'a, x: int, y: int} => {
let dx = b->x - a->x;
let dy = b->y - a->y;
sqrt(dx * dx + dy * dy);
};
/**
* Not punned AND type annotated.
*
* Notice 'a and 'b are different, hopefully we can calculate the distance
* between any two objects responding to the messages `x` and `y`.
*/
let distanceBetweenObjects
a:aa:{..'a, x: int, y: int}
b:bb:{..'b, x: int, y: int} => {
let dx = bb->x - aa->x;
let dy = bb->y - aa->y;
sqrt(dx * dx + dy * dy);
};
/**
* Destructuring records without type annotations (type inferred as
* `characterT3`).
*
* Is this an unnamed argument with a type declaration OR is it a named argument
* where the internal name is a destructuring pattern? Can the parser tell the
* difference? Can people tell the difference?
* Ocaml doesn't allow unnamed record types but maybe it'll come, as people seem
* to want it (well I certainly do).
*/
let distanceBetweenRecords
a:{x: x1, y: y1}
b:{x: x2, y: y2} => {
let dx = x2 - x1;
let dy = y2 - y1;
sqrt(dx * dx + dy * dy);
};
let distanceBetweenRecordsUnnamed
a:characterT3
b:characterT3 => {
let dx = b.x - a.x1;
let dy = b.y - a.y;
sqrt(dx * dx + dy * dy);
};
/* Call syntax isn't any different from Jordan's proposal */
let dist = distanceBetweenObject a:myCharacterObject b:myCharacterObject;
let dist = distanceBetweenRecords a:myCharacterRecord b:myCharacterRecord;
/* Unnamed arguments */
let dist = distanceBetweenRecordsUnnamed myCharacterRecord myCharacterRecord;
/**
* Random thought: What if we had object destructuring and no type annotation?
*/
let distanceBetweenObjects
a:{..'a, x: x1, y: y1}
b:{..'a, x: x2, y: y2} => {
let dx = x2 - x1;
let dy = y2 - y1;
sqrt(dx * dx + dy * dy);
};
/**
* This ^ could internally become the following. (Like what babel does to bring
* ES6 destructuring in the ES5 world).
*/
let distanceBetweenObjects
:a:{..'a, x: int, y: int}
:b:{..'b, x: int, y: int} => {
let x1 = a->x;
let y1 = a->y;
let x2 = b->x;
let y2 = b->y;
let dx = x2 - x1;
let dy = y2 - y1;
sqrt(dx * dx + dy * dy);
};
@jordwalke
Copy link

  • It seems that {. for closed objects and {..'a for open objects does help

Clarification, {./{..'a were proposed for closed/open object types.

I went with the idea of using -> instead of Jordan's idea of using
obj.methodName: just to explore an alternative. I haven't been able to come
up with a reason why -> is simpler or better than Jordan's idea

I'm still liking obj.methodName: because it also blends so well with named arguments of the same pattern:

obj.methodName: namedArg:10 anotherNamedArg:20 "finalArg";

However, there's another possible form for object method calls: Using x.methodName for method calls
and record->recordField for calling record fields as functions. I propose this because invoking functions on
records is surprisingly pretty rare! Just a thought.

(ps: I really wish we could destructure objects, see bottom)

I don't think it's too hard. For example:

let myFunc {:objField, :anotherObjField} => objField + anotherObjField;
/* Could just become (you have to deal with variable renaming safely though) */
let myFunc o => {
  let objField = o->objField;
  let anotherObjField = o->anotherObjField;
  objField + anotherObjField;
};

One thing that jumps out at me about using {.. } to represent object values is that ... and .. mean "and there's more stuff to merge in". For example, with records {...otherRecord, thisField: 0} means, "include thisField, and there's more stuff to merge in - you'll find it in otherRecord". For the object type proposal I made in Sander's diff, .. also means "and there's more. {..a', thisField: unit => int} means "and there's more fields in this type, and you'll find it in the type of 'a". So my first reaction is that using {.. someField} to represent an object value isn't consistent with the understood implication of ../... always meaning "and there's more". In the case of object literals there really isn't more - what you see is what you get.

Syntactically, there isn't any problem implementing your proposal - there aren't any grammar ambiguities.

The benefits of your proposal are that:

  1. The syntax for object type definitions and object literals look similar.
  2. Types and values resemble records once you overlook the leading .. in object literals.

The downsides of your proposal are:

  1. -> is a little funky (you could imagine dropping that part of your proposal, and doing something like obj.methodName:).
  2. The .. is misleading.
  3. You still need to include pri sometimes (not really a downside of your proposal - just a thing left unsolved that other proposals don't solve either).

There's some questions/problems you brought up here with named argument syntax - some of that is just fault of the named argument syntax proposal.

@bsansouci
Copy link
Author

Thanks for summarizing it.

I'm not totally sure why -> is funky :p. I find it more familiar than myObject.myMethod: 10 but I'm not really opinionated about it.

.. is definitely misleading put that way. I was really excited about a way to have "data" be expressed as simply in objects as it is in records, key value pairs.

What's the problem with private? Is there really any way to get rid of it?

Also, when we force pub and pri, what do we do for data values?

After some thought, overall, it might not be a good idea to encourage the usage of objects. The added complexity (class types, inheritance, message passing paradigm) isn't worth the row polymorphisms benefits and the inlined objects (without classes). Seeing the future of ocaml which will allow anonymously typed records (don't know how to call that), we can imagine adding some magical row polymorphism to records. The question that I find bothersome is: would row polymorphism decrease "type safety" but increase development velocity and extensibility? If that's the balance we're looking at, which one to favor?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment