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 typeAand 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 variables:
foo: ${ bar = 2, qux: i4 }
- Classes are objects too: Classes can have a static
companion(or not) that implements interfaces and behaves just like regular singleton objects. - Term and Expression scoping: The right hand side of an operator has either term scope (short right) or expression scope (long right). A term is a single item and an expression is a sequence of terms connected by operators. A term is one of the following.
- A name
- A number
- A string
- A tuple
- An array
- A block
- A monadic function and its operands
- 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
{
print:Console/WriteLine
Life:class {
w->i4, h->i4, grid->Grid(bit)
mod(n:i4 max:i4):{
n<0 ?++ n<-(_+max),
n≥max ?++ n<-(_-max),
^:n
}
at(x:i4 y:i4): grid(mod(x w) mod(y h))
get(x:i4 y:i4): at(x y)/Get()
set(x:i4 y:i4 b:bit): at(x y)/Set(b)
new(w:i4 h:i4):Life{
{w h} := _arg,
grid := Grid(bit)/ctor(w h),
}
txt:StrBuild/ctor()
update(): {
g:get,
txt/Clear(),
ta: txt/Append,
nextGrid:Grid(bit)/ctor(w h),
↕h|?(y:i4){
↕w|?(x:i4){
w:x-1 n:y+1 e:x+1 s:y-1,
c:[w:n x:n e:n w:y e:y w:s x:s e:s]|g⌗⟙,
live:(c=3)∨(g(x y)∧(c=2)),
nextGrid(x y)/Set(live),
ta(live ?+ "█" ?- " ")
}
ta(newline)
}
grid := nextGrid
}
}
main(args:str)→i4: {
life:Life/new(24 24),
[0:2 1:2 2:2 2:1 1:0]|?(x y):life/set(x+4 y+4 yes),
run:label,
life/update(),
Console/{
Clear(),
Write*life/txt/val,
},
go(run)
}
}
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 operations like
ret
Oblivia has these basic data types:
bit: Boolean,yesandnoi8: 8 byte signed integerf8: 8 byte signed float
Infix arithmetic is available for common operations (see dyadic functions). Note that B is termwise.
A + BA - BA × BA ÷ BA > BA < B
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. IfBis a type, then the value is a placeholderA -> B: Declare field A with type BA() -> B: Declare method A has type B`A -> B: C:A() -> B: C:A!: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: ReturnAfrom the current scope.^^: A: ReturnAfrom the parent scope.^^^: A: ReturnAfrom 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): IfAis a generic type, thenA(B)is the fully parametrized version of the type. IfAis a non-generic or fully parametrized (e.g. does not accept generic arguments) type, then calling it simply castsBto that type.
A: Get value of identifierAfrom the latest scope that defines it (current scope, then parent scope, then parent-parent scope)^A: Get value of symbolAfrom the current scope^^A: Get value of symbolAfrom the parent scope.^^^A: Get value of symbolAfrom parent's parent scope.'A: Alias of expressionA. Assignments on variableB:'Awill 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 statementsAto 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, evalDA(B)A[B]:A([B])A{B}:A({B})CallAwith the result of{B}- If
Ais a class, then constructs an instance ofAand applies the statementsBto it.
- If
A-B: Range from A to BA->B: Function typeA..B: Eval A, assign to _, then eval BA.B:A(B)CallAwith arg termB(no spread)A*B:A(B)CallAwith arg expressionB(spread if tuple)A/B: In the scope of expressionAevaluate expression B. Cannot access outer scopes.A/{B}: In the scope of expressionA, evaluate statementsB. Can access outer scopes.A/ctor: From .NET typeAget the unnamed constructor.A|B: Map array A by function B.A?|B: Filter A by B?(): A: Creates a lambda with no arguments and outputA?(A): B: Creates a lambda with argumentsAand outputBA.|B:B|ACallAwith every item from termB(no spread)A*|B:B|ACallAwith 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 inAcall with arg termBA|*B:A|?(a):a(B)From every item inAcall with arg expressionBA|/B:A | ?(a) a/BFrom every item inAget 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)istruethen includeCin 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 ifAequalsBA =- B: Returns true ifAdoes not equalBA = B: Returns true ifAmatches patternBA = B:C: Returns true ifAmatches patternBand assigns the value toCA =: B: ifA = typeof(B), then setsA := Band returns true
$A: ObjectA$(A:B): ObjectAof typeB$(A): Object of typeA$[A B C]: ItemsA,B,C$[A:B C:D]: ItemsAof typeBandCof typeD; make localA:BandC:D$[A=B C=D]: ItemsA,Csuch thatA = BandC = D${ A = B }: Object memberAof typeB${ A = B:C }: Object memberAof typeB; make localC:B(A)${ A:B }: Object memberAof typeB; defineA:BA${ B:C } = all($(A), ${ B:C })
yes: Trueno: False⟙: True⟘: False∅: Emptyempty: Automatically removed when added to a tuple or array
↕A: Returns[0 1 2 ... A]⍋A: Returns[n n+1 n+2 ... m]where[A(n) A(n+1) A(n+2) ... A(m)] = sorted(A)⍒A: Returns[n n+1 n+2 ... m]where[A(m) ... A(n+2) A(n+1) A(n)] = sorted(A)⌈A:ceil(A)⌊A:floor(A)A⋖: First element ofAA⋗: Last element ofAA⌗: Returns length ofA⚄A: Returns a random item from[0 1 2 ... A]⌨A: Returns the value of the first char ofA¬A: Returnsnot(A)
All term-scoped
A⌗B: Returns count ofBinAA∀B: ReturnsA ?| B⌗ = A⌗A∃B: ReturnsA ?| B⌗ > 0A∄B: ReturnsA |? B⌗ = 0A⫷B: Constructs classAwith dataBA∨B: Returns[||:A B]A∧B: Returns[&&:A B]A⋃B: Returnsany(A B)A⋂B: Returnsall(A B)A≤B:A≥B:
- Whitespace is the simplest operator.
- Two adjacent identifiers
A Bsimply means thatAoccurs beforeBin 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 Bsuch thatA,Bare identifiers andAperforms 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.
- 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