Oblivia (OBL) is an esolang that aims to do with objects what Lisp does with lists. Oblivia follows these ideas:
- Terse syntax:
- A small set of primitive operations are given the most terse syntax.
- Casting values is as easy as
A(B)
with typeA
and valueB
. - Defining variables is simply
A:B(C)
with variable nameA
, typeB
, and valueC
.
- Scopes = Objects: In Oblivia, scopes have the power of objects.
- Pass a scope to a function.
- Name a variable or a member and it automatically becomes a key of the scope.
- A scope with no explicit return simply returns itself as an object (if it has keys) or the last expression (no keys).
- Objects = Functions: In Oblivia, any object can define the
(), [], {}
operators. - Variables of any name: Variable names are not restricted by those already defined in the outer scope. To access variables of an outer scope, Oblivia introduces the
^
"up" function - Same syntax everywhere:
:
is the define operatorA(B), A[B], A{C}
is a function call.- Patterns can be stored in objects
- Classes are objects too: Classes can have a static
companion
(or not) that implements interfaces and behaves just like regular singleton objects.
- JavaScript: Destructuring
- CSharp: Tuples
- APL: short-left, long-right precedence
The following code implements a Conway's Game of Life and updates until the count of active cells becomes unchanging
{
Life:class {
width:int height:int grid:Grid.bool
adj(n:int max:int): modi(lt(n 0) ?+ addi(n max) ?- n max)
at(x:int y:int): array_at(grid adj.|[(x width),(y height)] |:int ?(i:int) i)
get(x:int y:int): at(x y)/Get!
set(x:int y:int b:bool): at(x y)/Set.b
new(width:int height:int): Life {
(width height) := ^^(width height)
grid := Grid.bool/ctor(width height)
debug!
}
debug!: {
print*cat["width: " width]
print*cat["height: " height]
}
activeCount:0
txt: StringBuilder/ctor!
update!: {
activeCount := 0
g: get
txt/Clear!
range(0 height) | ?(y:int) {
range(0 width) | ?(x:int) {
w:subi(x 1) n:addi(y 1) e:addi(x 1) s:subi(y 1)
c: count(g.|[
(w n),(x n),(e n),
(w y), (e y),
(w s),(x s),(e s),
] true)
active:g(x y)
active:= _ ?+ {
lt(c 2) ?+ false ?-
gt(c 3) ?+ false ?- _
} ?- {
eq(c 3) ?+ true ?- _
}
set(x y active)
activeCount := active ?+ addi(_ 1) ?- _
str_append(txt active ?+ "+" ?- "-")
}
str_append(txt newline)
}
print*cat["active: " activeCount]
}
}
main(args:string): int* {
life:Life/new(32 32)
print*array_at(life/grid, [:int 0 0])/Get!
range(0 life/width) | ?(x:int) range(0 life/height) | ?(y:int)
life/set(x y rand_bool!)
count:1 prevCount:0 run:true
Console/Clear!
run ?% {
life/update!
prevCount := count
count := life/activeCount
run := neq(count prevCount)
Console/SetCursorPosition(0 0)
print*str*life/txt
}
^: 0
}
}
Oblivia has 3 basic structures.
- Array: Contains a sequence of items and nothing more.
- Tuple: Contains a sequence of items, some with string keys.
- Block: Contains a set of variables with string keys. Supports advanced operations such as
ret
Lisp-like arithmetic allows you to spread operands. Operators are converted to reductions e.g. [+: a b c] = a/\+(b)/\+(c) = reduce([a b c] ?(a b) a/\+(b))
[+: a b]
[-: a b]
[*: a b]
[**: a b]
[/: a b]
[//: a b]
[^: a b]
[%: a b]
[=: a b]
[>: a b]
[<: a b]
[~: a b]
[>>: a b]
[<<: a b]
[&: a b]
[|: a b]
[&&: a b]
[||: a b]
A:B
: field A has value B. IfB
is a type, then the value is a placeholderA!:B
: function A with no args has output BA(B, C): D
: function A with args B,C has output DA[B C]: D
A{B C}: D
A := B
: reassign field A to B (same type). You can use_
for the current value ofA
^: A
: ReturnA
from the current scope.^^: A
: ReturnA
from the parent scope.^^^: A
: ReturnA
from the parent's parent scope.
A! = A()
: call A with 0 argsA*B = A(B)
: call A with arg B (associative-right)A.B = A(B)
: call A with arg B (associative-left)A(B C)
: call A with args B, CA.B.C.D = ((A*B)*C)*D = ((A(B))(C))(D)
A*B*C*D = A(B(C(D)))
A(B)
: IfA
is a generic type, thenA(B)
is the fully parametrized version of the type. IfA
is a non-generic or fully parametrized (e.g. does not accept generic arguments) type, then calling it simply castsB
to that type.
A
: Get value of identifierA
from the latest scope that defines it (current scope, then parent scope, then parent-parent scope)^A
: Get value of symbolA
from the current scope^^A
: Get value of symbolA
from the parent scope.^^^A
: Get value of symbolA
from parent's parent scope.'A
: Alias of expressionA
. Assignments on variableB:'A
will attempt to assign toA
.[A B C]
: Make an object array[A:B C:D] = [(A B), (C D)]
[:type A B C]
: Make an array oftype
{ A }
: Creates an scope and applies the statementsA
to it. If the scope has no locals or returns, then the scope returns the result of the last statement (empty if no statements). Otherwise returns an object.A ?+ B ?- C
: If A then B else C.A ?++ B ?+- C ?-- D
: While A, evaluate B. If at least one iter, evalC
. If no iter, evalD
A(B)
A[B]
:A([B])
A{B}
:A({B})
CallA
with the result of{B}
- If
A
is a class, then constructs an instance ofA
and applies the statementsB
to it.
- If
A-B
: Range from A to BA->B
: Function typeA..B
: Eval A, assign to _, then eval BA.B
:A(B)
CallA
with arg termB
(no spread)A*B
:A(B)
CallA
with arg expressionB
(spread if tuple)A/B
: In the scope of expressionA
evaluate expression B. Cannot access outer scopes.A/{B}
: In the scope of expressionA
, evaluate statementsB
. Can access outer scopes.A/ctor
: From .NET typeA
get the unnamed constructor.A|B
: Map array A by function B.?(): A
: Creates a lambda with no arguments and outputA
?(A): B
: Creates a lambda with argumentsA
and outputB
A.|B
:B|A
CallA
with every item from termB
(no spread)A*|B
:B|A
CallA
with every item from expressionB
(spread if tuple)A/|B
:?(C) B(A C)
A/|B(C)
:B(A C)
A|.B
:A|?(a):a(B)
From every item inA
call with arg termB
A|*B
:A|?(a):a(B)
From every item inA
call with arg expressionB
A|/B
:A | ?(a) a/B
From every item inA
get value of symbolB
.A||B
:?(C) A | ?(a) B(a C)
A||B(C)
:A | ?(a) B(a C)
A ?[ B0:C0 B1:C1 B2:C2 B3:C3 ]
: Conditional sequence; for each pairB:C
, ifA(B)
istrue
then includeC
in the result.A ?{ B0:C0 B1:C1 B2:C2 B3:C3 D0 D1 D3 }
: Match expression (naive): For each pairB:C
, ifA = B
, then returnsC
. Can also accept lambdaD
?{ A0:B0 }
: Matcher functionA =+ B
: Returns true ifA
equalsB
A =- B
: Returns true ifA
does not equalB
A = B
: Returns true ifA
matches patternB
A = B:C
: Returns true ifA
matches patternB
and assigns the value toC
A =: B
: Returns true ifA = typeof(B)
- Whitespace is the simplest operator.
- Two adjacent identifiers
A B
simply means thatA
occurs beforeB
in a sequence. Identifiers are never grouped together outside of tuples and arrays.{ A B:C D } = { A:A, B:C, D:D }
,{ (A B):(C D) } = { A:C, B:D }
- There is never a statement of the form
A B
such thatA
,B
are identifiers andA
performs some operation onB
, other than occurring earlier in a sequence.
- Two adjacent identifiers
- Function calls with 0/1 arguments are allowed alternate syntax to save pixels
- Calls with n>1 arguments always require enclosing delimiters
A(B C)
- Calls with 0, 1 arguments are allowed single-ended operators
A!
,A*B
,A.B
- Calls with n>1 arguments always require enclosing delimiters
OBL emphasizes generality and terseness, rejecting features that are not versatile enough to justify the syntax cost.
- Infix arithmetic: Fails when you need to reduce an array, making the procedure unnecessarily verbose.
(+: a b)
Lisp-like arithmetic solves this by making arithmetic spreadable.
- Partial application / whatever-priming (Raku): Scope-constrained to simple expressions. Cannot control argument order.
?(<par>) <expr>
Lambdas solve this problem by forward declaring arguments and allowing any size scope.
=
-based assignment: This function is often confused between assignments and boolean comparisons.- OBL uses
:=
, where:
makes it clear that this function is strictly assignment.=
is a pattern match function.
- OBL uses