class / defclass
June 4, 2025 ยท View on GitHub
The class special form creates a JavaScript class expression. defclass is a
macro which assigns the class value to a Piglet var of the same name as the
class.
(Note that class is only used to create JS class expressions, not to find the
"class" of an object, for that use type.)
(defmacro defclass
"Define a JavaScript class"
[class-name & body]
`(def ~class-name (class ~class-name ~@body)))
Syntax overview
(class ; or defclass
;; name for the Piglet var, and for the class itself. Optional when using `class` expression
my-class
;; inherit from other class, SuperClass can be any expression
:extends SuperClass
;; fields with and without inital value, static and non-static
:fields [a, (b 123), ^:static 123, ^:static (d 456)]
;; Not sure yet about this bit. It's a neat bit of overloading, but what if you
;; want to emit this kind of code from a macro? I guess you can do a double
;; unquote?
:fields [~(str "-" i)] ; computed property name
:fields [(~(str "-" i) "foo")] ; computed property name with default value
;; Maybe this works? mimic JS itself?
:fields [[(str "-" i)]] ; computed property name
:fields [([(str "-" i)] "foo")] ; computed property name with default value
;; Static initializer block
:init
(do ,,,) ; takes any expression, use `do` if you want to do multiple things
;; Property getters and setters
:get (vvv [] ,,,)
:set (vvv [v] ,,,)
;; Constructor, `super` and `this` can be used in the body
(constructor [arg]
(super ,,,)
(set! (.-xxx this) ,,,)
,,,)
;; Plain method, instance and static, `this` is available in the function body
(some-method-name [arg])
(^:static some-static-method-name [arg])
([(str hello "-" world)] [arg] ,,,) ; computed method name
;; async method, can use await
(^:async some-async-method [arg])
;; Protocol implementations
MyProtocol
(-foo [,,,] ,,,)
)
Step by step:
(class) ;=> class {}
(class foo) ;=> class foo {}
(class foo :extends Bar) ;=> class foo extends bar {}
(class foo (my_method [arg]))
;;=> class foo {
;; my_method(arg6) {
;; }
;; }
(class foo (my-method [arg]))
;;=> class foo {
;; ["my-method"](arg6) {
;; }
;; }
(class foo (^:static my_method [arg]))
;;=> class foo {
;; static my_method(arg6) {
;; }
;; }
(class foo :extends Object (constructor [arg] (super arg)))
;; class foo extends Object {
;; constructor(arg9) {
;; return super(arg9);
;; }
;; }
(class :fields [a]) ;;=> class {a;}
(class :fields [(a 1)]) ;;=> class {a = 1;}
(class :fields [^:static a]) ;;=> class { static a ; }
(class :fields [^:static (a 1)]) ;;=> class { static a = 1; }
(class :fields [(~(str "xxx") 1)]) ;;=> class { ["xxx"] = 1; }
(class :fields [(~(str "xxx") 1)]) ;;=> class { ["xxx"] = 1; }
(class X :set (foo [v] (set! this.bar v)))
;; class X {
;; set foo(v20) {
;; return this.bar = v20;
;; }
;; }
(class X :get (foo [] this.bar))
;; class X {
;; get foo() {
;; return this.bar;
;; }
;; }
(class X ([js:Symbol.iterator] [] ,,,)) ;;=> make a class iterable
Computed properties
In JavaScript in both object literals and class expressions you can use an extra pair of square brackets to indicate that a given property name is computed on the fly.
const foo = class {
[`_${2*2}`] = 4;
["_xxx"]() {
return 9
}
}
We adopted the same syntax, wherever you would have a field or method name in a
class or defclass, you can introduce an extra pair of brackets to indicate
that the name should be computed. It's a little bit like an unquote (~),
you're opting in to evaluation of something that normally is static.
(class foo :fields [a]) ;; base case, not computed
(class foo :fields [[(str "a")]]) ;; computed
(class foo :fields [(a 4)]) ;; with default value
(class foo :fields [([(str "a")] 4)]) ;; with default value, computed
(class foo
(bar [])) ;; method definition
(class foo
([(str "bar")] [])) ;; computed method definition
Static
JS has three ways the static keyword can be used in a class definition, all
indicating that something is defined on the class (prototype) level, and not on
the instance level
Static initializer blocks are a top-level syntactic construct, for these we use
the :static keyword, similar to how we have :extends or :fields. :static
needs to be followed by a single form. Use do if you want to execute multiple
statements.
(class Foo
:static
(do
(set! this.bar 123))) ;; class level attribute
For fields and methods, you use a ^:static metadata tag on the field or method
name.
(class Foo
:fields [^:static bar]
(^:static baz [] ,,,))
If the field has a default value, then ^:static still goes on the name of the
property.
(class Foo
:fields [(^:static bar 123)])
Putting it outside the list form also works, but it's easiest to remember that
^:static always goes directly in front of the name.
In case of computed properties/methods, :^static goes before the wrapping
vector.
(class Foo
:fields [(^:static ["bar"] 123)])
Protocols
Piglet protocols are based on js:Symbol instances, which we call sentinels.
There's a sentinel for each protocol method, and one for the protocol itself.
When a protocol's dispatch function is called, it looks for these sentinels to
find the right method implementation.
(defclass foo
Counted
(-count [this] 3))
This code is equivalent to (using computed properties)
(defclass foo
:fields [([(.-sentinel Counted)] true)]
([(.method_sentinel Counted "-count" 1)] [this] 3))
(count. foo) ;;=> 3
Piglet knows that you're extending a protocol when it encounters a symbol inside
the class form. This leads to a potential ambiguity when defining an anonymous
class.
(let [x (class Counted (-count [this] 5))]
(count (x.)))
In this case Piglet will assume you are trying to create a class named
Counted, rather than extending the Counted protocol. Because of this, and
for consitency with the rest of the class syntax, you may prefix protocol
implementations with :implements
(let [x (class :implements Counted (-count [this] 5))]
(count (x.)))
make-type / deftype
There are also two macros, make-type and deftype, which mimic the above
syntax, but do so by generating prototype-based code. Rather than emitting
class .... This is often preferred, as it plays better with a REPL. JS class
forms can not be redefined.
The result looks like this:
(deftype A
(constructor []
(set! (.-foo this) 123)
nil)
(get_foo [] (.-foo this))
Counted
(-count [_] 3))
intern(qsym("https://arnebrasseur.net/pigrot:tilegrids:A"), (function () {
const ctor31 = function A() {
this.foo = 123;
return null;
};
const proto11 = {};
ctor31.prototype = proto11;
proto11["get_foo"] = function get_foo() {
return this.foo;
};
const object91 = proto11;
$piglet$["https://piglet-lang.org/packages/piglet"].lang.Counted.value.extend_object2(object91, {
"-count/1": function (_37) {
return 3;
}
});
object91;
return ctor31;
})()
)
(deftype B :extends A
(constructor [x]
(super)
(set! (.-x this) x)
nil))
intern(qsym("https://arnebrasseur.net/pigrot:tilegrids:B"), (function () {
const ctor41 = function B(x37) {
$piglet$["https://arnebrasseur.net/pigrot"].tilegrids.A.value.call(this);
this.x = x37;
return null;
};
const proto21 = {};
ctor41.prototype = proto21;
proto21.__proto__ = $piglet$["https://arnebrasseur.net/pigrot"].tilegrids.A.value.prototype;
return ctor41;
})()
)