Closuretalk

:Not to be confused with McCall and Tesler's TinyTalk language developed in 1980.
Closuretalk (formerly known as tinytalk) is a minimalistic, purely object-oriented programming language by User:Rdococ. It was designed to answer the question of "what is the most object-oriented language possible?".
== Semantics ==
Every value is an object. An object consists of a set of methods responding to various named messages and/or decorated objects whose methods are copied by the new object. Variables are created by definitions or as parameters in method signatures.
Numbers, strings, booleans and nil are provided as primitive objects. The builtin Cell and Array objects can be asked to create new mutable cells, and the console object can print:, write:, read a line of input or error:.
== Syntax ==
Closuretalk has a simplified form of Smalltalk syntax with an extra form added for object literals.
=== Statements & Variables ===
Expressions can be chained with ., typically returning the result of the last expression in the chain. A variable is defined with the := operator and can be assigned to with the <- operator. Variable definitions are hoisted but only assigned to when the definition is reached.
=== Object creation ===
Objects are created with [], and contain methods and decorations separated by |. A method is a method signature, such as attackedWith: weapon By: entity, followed by an expression. The syntax for decoration is ... followed by an expression.
=== Method syntax ===
Methods are called by specifying the receiver followed by a message and arguments.
* Unary messages are single words. For example, 'fraction' import imports the 'fraction' file, and x abs takes the absolute value of x.
* Binary operators are symbol characters. e.g. 2 + 3 and 9 / 2.
* Keyword messages consist of a set of words. Each word ends in : and leads into an argument. For example, Point atX: x Y: y. The initial word beings with a lowercase letter, and non-initial words begin with uppercase letters.
Unary messages have the highest precedence, followed by binary operators, and then keyword messages. Binary operators are left-associative and do not follow the order of operations. Keyword messages are also left-associative. The latter two rules are subject to change.
=== Blocks & explicit returns ===
Blocks are created with {}. This produces an object with a single method called do with special behaviour. An explicit return is constructed with ^ and an unchained expression, e.g. ^ x * 2. Explicit returns ignore block methods, making them useful for control flow constructs.
== Examples ==
=== brainfuck ===
'switch' import.
brainfuck := 
 [tokenizer: code
 ip := 0.
 self := [read
 ip <- ip + 1.
 char := code at: ip.
 switch on: char
 case: [when '+' | do [if: c c inc]]
 case: [when '-' | do [if: c c dec]]
 case: [when '<' | do [if: c c left]]
 case: [when '>' | do [if: c c right]]
 case: [when '[' | do [if: c c loop]]
 case: [when ']' | do [if: c c end]]
 case: [when ',' | do [if: c c input]]
 case: [when '.' | do [if: c c output]]
 case: [when | do [if: c c end]]
 else: [do self read]].
 |compile: code
 tokenizer := brainfuck tokenizer: code.
 self :=
 [continue
 tokenizer read if:
 [inc self compose: [instruct: m m inc] With: self continue.
 |dec self compose: [instruct: m m dec] With: self continue.
 |left self compose: [instruct: m m left] With: self continue.
 |right self compose: [instruct: m m right] With: self continue.
 |loop 
 body := self continue.
 self compose: [instruct: m m loop: body] With: self continue.
 |end [instruct: m]
 |input self compose: [instruct: m m input] With: self continue.
 |output self compose: [instruct: m m output] With: self continue]
 |compose: a With: b
 [instruct: m
 a instruct: m.
 b instruct: m]].
 program := self compose: self continue With: [instruct: m m end].
 |interpreter
 tape := Array make.
 tape at: 1 Put: 0.
 tp := 1.
 len := 1.
 self := [inc tape at: tp Put: (tape at: tp) + 1 % 256.
 |dec tape at: tp Put: (tape at: tp) - 1 % 256.
 |left tp <- tp - 1.
 tp < 1 if: [true console error: 'Tape pointer exceeded left edge' | false].
 |right tp <- tp + 1.
 tp > len if:
 [true len <- len + 1. tape at: tp Put: 0.
 |false].
 |loop: body
 (tape at: tp) > 0 if:
 [true body instruct: self. self loop: body.
 |false]
 |input tape at: tp Put: (console read: 1) byte.
 |output console write: (tape at: tp) character.
 |end tape]
 |luaBackend
 rawCode := 'tape, tp = {0}, 1;'.
 code := [append: piece rawCode <- rawCode, piece | value rawCode].
 self := [inc code append: 'tape[tp] = (tape[tp] + 1) % 256;'
 |dec code append: 'tape[tp] = (tape[tp] - 1) % 256;'
 |left code append: 'tp = tp - 1;'
 |right code append: 'tp = tp + 1;tape[tp] = tape[tp] or 0;'
 |loop: body code append: 'while tape[tp] > 0 do '.
 body instruct: self.
 code append: ' end;'
 |input code append: 'tape[tp] = io.read(1):byte();'
 |output code append: 'io.write(string.char(tape[tp]));'
 |end code value]].
This is an extensible brainfuck compiler. You use brainfuck compile: to compile code into a program object. The program object can be told to instruct: a machine object on what to do with the [inc | dec | left | right | loop: body | input | output | end] interface. This machine can interpret the program, as with brainfuck interpreter, compile it into another language like brainfuck luaBackend, or do basically anything else with it.
=== Shapes ===
This is an implementation of shapes as objects. Every shape is expected to return its top left and bottom right points with origin and corner, and return if it contains a specific point with contains:. This can be used to represent all kinds of possible shapes, including squares, circles, and arbitrary combinations of shapes as demonstrated below.
'Vector' import.
Square :=
 [at: origin WithSize: size
 corner := origin + (Vector makeX: size Y: size).
 [contains: point
 point >= origin and: point < corner
 |origin origin
 |corner corner]].
Circle :=
 [at: centre WithRadius: radius
 [contains: point (point - centre) size < radius
 |origin Vector makeX: centre x - radius Y: centre y - radius
 |corner Vector makeX: centre x + radius Y: centre y + radius]].
Union :=
 [of: shapeA And: shapeB
 [contains: point shapeA contains: point or: (shapeB contains: point)
 |origin shapeA origin origin: shapeB origin
 |corner shapeA corner corner: shapeB corner]].
Intersection :=
 [of: shapeA And: shapeB
 [contains: point shapeA contains: point and: (shapeB contains: point)
 |origin shapeA origin corner: shapeB origin
 |corner shapeA corner origin: shapeB corner]].
The information provided by the [contains: | origin | corner] interface is enough to draw the shape to the console. Here is a Drawer implementing just that.
Drawer :=
 [draw: shape
 yLoop := [on: y
 xLoop := [on: x
 console write: (shape contains: (Point atX: x Y: y) if: [true '#' | false ' ']).
 x < shape corner x ifTrue: [do
 xLoop on: x + 1]].
 xLoop on: shape origin x.
 console print: .
 y < shape corner y ifTrue: [do
 yLoop on: y + 1]].
 yLoop on: shape origin y].
You could create other drawer objects that support draw: if you wanted to support more graphical forms of display. Shapes could return the colour they have at a specific point rather than a boolean, enabling them to support multiple colours. At that point, though, you would have a more general 'image' interface supporting both bitmap and vector graphics at arbitrary resolutions, or graphics stored remotely on an Internet server and cached by the image itself.
=== Sets ===
A set can be defined in an object-oriented way. The interface for sets as implemented below is [empty | contains:]. empty returns true or false depending on if the set is empty, and contains: returns true if the set contains a value. You can define infinite sets, such as the set of all natural numbers or odd numbers. You can define the set of all sets that do not contain themselves, but it can result non-termination.
nats := [empty false
 |contains: n
 n > 0 and: n floor = n].
odds := [empty false
 |contains: n
 nats contains: n and: n % 2 = 1].
console print: (odds contains: 1). "true"
console print: (odds contains: 2). "false"
console print: (odds contains: 3). "true"
console print: (odds contains: 4). "false"
console print: (odds contains: 5). "true"
russell := [empty false
 |contains: set
 (set contains: set) not].
console print: (russell contains: russell). "infinite loop!"
You could define a Set object that can make: useful sets by decorating naked set objects with useful operations like filter:.
=== Lists ===
While Closuretalk is purely object-oriented, you can define data structures by simply implementing objects with plain getters, setters and/or visitors. Here's an example of a list data type.
List :=
 [with: first Before: rest
 self := [if: cases cases with: first Before: rest
 |map: fn List with: (fn of: first) Before: (rest map: fn)
 |fold: fn Onto: id fn of: first And: (rest fold: fn Onto: id)
 |makeString 'List(', first makeString, rest makeRestString
 |makeRestString ', ', first makeString, rest makeRestString]
 |empty
 self := [if: cases cases empty
 |map: fn List empty
 |fold: fn Onto: id id
 |makeString 'List()'
 |makeRestString ')']].
ls := (List with: 1 Before: (List with: 2 Before: (List with: 3 Before: List empty))).
mls := ls map: [of: x x * 2].
fls := ls fold: [of: x And: y x * y] Onto: 1.
 
console print: ls. "List(1, 2, 3)"
console print: mls. "List(2, 4, 6)"
console print: fls. "6"
The List constructors ensure that all lists are finite, but you can always feed in custom implementations of infinite lists to objects expecting finite lists to cause infinite loops. All data types are essentially bullied into laziness.
=== Vectors ===
Vector := [makeX: x Y: y
 a := [+ b Vector makeX: x + b x Y: y + b y
 |- b Vector makeX: x - b x Y: y - b y
 |size (x * x + (y * y)) sqrt
 |makeString 'Vector(', x, ', ', y, ')'
 |x x
 |y y
 |origin: b Vector makeX: (x smaller: b x) Y: (y smaller: b y)
 |corner: b Vector makeX: (x larger: b x) Y: (y larger: b y)
 |< b a x < b x and: a y < b y
 |= b a x = b x and: a y = b y
 |> b b < a
 |<= b a x <= b x and: a y <= b y
 |>= b b <= a]].
=== Switch construct ===
This is very useful when you want to dispatch on the value of builtin "data" objects like numbers and strings.
switch :=
 [case: case
 case if if:
 [true self := [case: c self | else: c case do]
 |false switch]
 |else: case
 case do
 |on: value
 decorator := [for: s [case: c decorator for: (s case: [if c if = value | ...c]) | ...s]].
 decorator for: switch].
x := console read makeNumber.
switch
case: [if x = 1 | do 'x is 1!']
case: [if x = 2 | do 'x is 2!']
else: [do 'x is not 1 or 2!'].
=== FizzBuzz ===
This sophisticated, object-oriented FizzBuzz supports arbitrary fizzbuzzes with any number of keywords.
FizzBuzz := [new
 rules := Array new.
 ruleCount := 0.
 self := [for: n
 result := .
 
 loop := [on: i
 rule := rules at: i.
 n % rule divisor = 0 ifTrue: [do
 result <- result, rule string].
 i < ruleCount if: [true
 loop on: i + 1 | false]].
 loop on: 1.
 
 result = if: [true n asString | false result]. 
 | add: rule
 ruleCount <- ruleCount + 1.
 rules at: ruleCount Put: rule]
| default
 fb := FizzBuzz new.
 fb add: [divisor 3 | string 'Fizz'].
 fb add: [divisor 5 | string 'Buzz'].
 fb.
| test
 fb := FizzBuzz default.
 
 loop := [on: n
 console print: (fb for: n).
 n < 100 ifTrue: [do
 loop on: n + 1]].
 loop on: 1.
 
 console print: 'Test done!'].
== Implementations ==
There is a reference implementation written in & transpiling to Lua. It includes a REPL, from which you can 'xxx' import to load the included examples and play around with them. Additional examples include a custom Fraction number type.
== Conclusion ==
An interesting thought experiment. Whether this is "the most object-oriented language possible" depends on your definition of OOP.
Compared to other paradigms, OOP uses:
* Dispatch 'by default': Instead of programming with data and using closures only where polymorphism is required, you program with closures and delegate only where monomorphism is required.
* Single dispatch only: Instead of multiple dispatch, you employ single dispatch where possible, and implement the visitor pattern (coincidentally also the mechanism to implement plain data) to emulate multiple dispatch.
By these two metrics, Closuretalk is purely object-oriented. But further simplifications can be made to the language itself - for example, in Self, there is an elegant unification between scopes and objects.
Category:Object-oriented paradigm Category:High-level Category:Turing complete Category:Turing tarpits Category:Implemented Category:2022