Adding dynamic type checking to Fennel. This library contains replacements for common built-in forms that accept type annotations and perform type checking at runtime.
Clone this repository and place it in your package path, e.g. the directory containing your code.
git clone https://github.com/dokutan/typed-fennel
(import-macros {: fn>} :typed-fennel)
(local {: has-type?} (require :typed-fennel))
;; simple type checking
(has-type? 1 :integer) ; => true
(has-type? 1 :float) ; => false
(has-type? [1 2] [:any :any]) ; => true
;; create a type checked function
(fn> inc [a :number] [:number] (+ a 1))
(inc 1) ; => 2
(inc "1") ; => runtime error
;; use let>, local> and var> for type checking
(var> x :string "hello")
(let> [a :number 10
b :number 20]
(+ a b))
;; destructuring is supported by all macros
(local> (a b) [:string :any] (foo))
(fn [a & rest] [:any :table]
(print a))
The available primitive types are based on the normal Fennel/Lua types, with some additions. They are represented as strings.
:nil
:string
:number
:boolean
:table
:function
:thread
:userdata
:integer
:float
:file
:closed-file
:any
All primitive types can be prefixed with a question mark, e.g. :?number
, to allow the corresponding value to be nil
. This also works for :?nil
and :?any
, which has no effect on the type, but could be useful for annotating optional function parameters.
(has-type? 1 :?number) ; => true
(has-type? nil :?number) ; => true
(has-type? "1" :?number) ; => false
Tables of types can be used to describe tables:
(local {: has-type?} (require :typed-fennel))
(has-type? [1] [:number :number]) ; => false
(has-type? [1 2] [:number :number]) ; => true
(has-type? [1 2 3] [:number :number]) ; => true
(has-type? [1 [2]] [:number [:number]]) ; => true
(has-type? {:x 1 :y 2} {:x :number :y :number}) ; => true
Use (seq T)
to create a type that describes a sequential table containing 0 or more elements of type T
.
(local {: has-type? : seq} (require :typed-fennel))
(has-type? [] (seq :number)) ; => true
(has-type? [1] (seq :number)) ; => true
(has-type? [1 2] (seq :number)) ; => true
(has-type? [1 2 "3"] (seq :number)) ; => false
Use enum
to create a type that allows a variable to take only one of the given values.
(local {: has-type? : enum} (require :typed-fennel))
;; A weekday enum
(local weekday
(enum
:monday
:tuesday
:wednesday
:thursday
:friday
:saturday
:sunday))
(has-type? :monday weekday) ; => true
(has-type? :foo weekday) ; => false
Any function (or table with the __call
metamethod) that accepts one value and returns a truthy value can be used as a type.
(import-macros {: fn>} :typed-fennel)
(local {: has-type?} (require :typed-fennel))
(fn positive-int [value]
"A positive integer type"
(and
(has-type? value :integer)
(>= value 0)))
(has-type? 1 positive-int) ; => true
(has-type? -1 positive-int) ; => false
(fn byte [value]
"A bounded type: 0-255"
(>= 255 value 0))
(has-type? 255 byte) ; => true
(has-type? 256 byte) ; => false
Union types can be constructed using the union
function:
(import-macros {: fn>} :typed-fennel)
(local {: has-type? : union} (require :typed-fennel))
(local number-or-string (union :number :string))
(has-type? 1 number-or-string) ; => true
(has-type? "1" number-or-string) ; => true
(fn> length2 [a (union :string :table)] [:integer] (length a))
(length2 "123") ; => 3
(length2 123) ; => runtime error
Union types can be constructed using the intersection
function:
(import-macros {: fn>} :typed-fennel)
(local {: has-type? : intersection} (require :typed-fennel))
(local ascii-char (intersection :string #(= 1 (length $))))
(has-type? :a ascii-char) ; => true
(has-type? :λ ascii-char) ; => false
In the fn>
macro any expression evaluating to a type can be used, this allows e.g. generics to be implemented.
When the macro is expanded, the expressions have access to the function parameters:
(import-macros {: fn>} :typed-fennel)
(fn> tuple [a :any] [[(type a) (type a)]] [a a])
(fn> assert-same-type [a :any b (type a)] [:boolean] true)
(assert-same-type 1 2) ; => true
(assert-same-type :1 :2) ; => true
(assert-same-type 1 :2) ; => runtime error