# The standard axle library. This is implicitly imported # into every program by the Axle interpreter. Note that # there is an even more primitive module called "prim" # which is defined in Python itself! (see primlib.py) # ___________________________________________________________________________ # Boolean combinations # # There is no built-in boolean type. Boolean values are generally tested # with `isFalse?` and `isTrue?`. By default, the only false value is `.false`, # and so any other value, such as lists or numbers, are all considered true. # > x isFalse? # Returns `.true` if `x` is `.false', and returns `.false` otherwise. # You may extend this function with addition cases to allow for other # kinds of values to be considered false. .false isFalse? = { .true } _ isFalse? = { .false } # > x isTrue? # Returns the boolean negation of `isFalse?`. There is no need to # extend this function, as it is based on `isFalse?`. (x isFalse?) isTrue? = { .false } _ isTrue? = { .true } # > bool or: func # > bool and: func # Define the common boolean combinators, using functions to enable # short-circuit evaluation. (_ isFalse?) and: (func isCallable?) = { .false } _ and: (func isCallable?) = { func() } (_ isFalse?) or: (func isCallable?) = { func() } _ or: (func isCallable?) = { .true } # > not: b # Logical inversion. Defined in terms of `isFalse?`. not: b = { b isFalse? } # > a != b # Define != by default based on the built-in ==. a != b = { (a == b) isFalse? } # ___________________________________________________________________________ # Control Flow # > if:cond then:func # Executes `func` depending on the value of `cond`. # Returns an empty list if cond `isFalse?`, or # a singleton list with the return value of `then` # otherwise. if:(_ isFalse?) then:(then isCallable?) = { [] } if:(_) then:(then isCallable?) = { [then()] } # > if:cond then:func1 else:func2 # Executes `func1` or `func2` depending on the value of `cond` # and returns the result. if:(_ isFalse?) then:(then isCallable?) else:(else isCallable?) = { else() } if:(_) then:(then isCallable?) else:(else isCallable?) = { then() } # > match:values with:cases # Cases should be a list of functions or blocks. They will be # invoked in turn until one is found that matches `values`. # Note that `values` should be a list, though it is generally # a singleton list (i.e., something like # `match:[x] with:[{:(i isInteger?) i}]`). # # If no matching case is found, throws an instance of # `NoMatchingCaseFound`. match:(values) with:[] = { throw:(NoMatchingCaseFound()) } match:(values) with:(hdtl:lst = [hd,tl]) = { res = tryToApply:hd to:values if:(res == []) then:{ match:values with:tl } else: { [val] = res val } } # > case:[ {a > b} -> { doSomething}, ... ] # Iterates over its argument, which should be a list of `Assoc` instances. # The key of each instance is a function that returns a boolean. If the # return value `isTrue?`, then the value of the instance, also a function, # will be evaluated. Otherwise, the next case is tried. If all cases return # false, an exception results. Include a final case `{.true} -> {...}` to # avoid the exception. case:[] = { throw:(NoMatchingCaseFound()) } case:(hdtl:_ = [currentCase, remainingCases]) = { if:(currentCase.key()) then: { currentCase.value() } else: { case:remainingCases } } # ___________________________________________________________________________ # Exceptions # > try:func tagError:tag # Tries function. If an exception `e` results, catches it and # rethrows `e taggedWith: tag`. Intended for use like: # > try:{ # > try:{...} tagError:1 # > ... # > try:{...} tagError:2 # > ... # > } catch:[ # > { :(_ isTaggedWith: 1) ... } # > { :(_ isTaggedWith: 2) ... } # > ] try:func tagError:tag = { try:func catch:[{:e throw:(e taggedWith: tag)}] } # > try:func ifSuccessfulDo:func2 # Executes `func`, catching and masking any exceptions. If no exceptions # are thrown, then executes `func2(r)` where `r` is the result of `func`. # Returns `[func2(r)]` if `func` does not throw any exceptions, and `[]` # otherwise. Intended for use when an error from `func()` is expected # and should be ignored, such as in a table lookup where the key may # not be present. try:func ifSuccessfulDo:succDo = { match:[try:{[func()]} catch:[{ :_ [] }]] with:[ { :[r] [succDo(r)] } { :[] [] } ] } # ___________________________________________________________________________ # Number predicates and operations # # The primitive library only defines n < m and the most basic mathematical # manipulations. (n isNumber?) > (m isNumber?) = { m < n } (n isNumber?) >= (m isNumber?) = { (n == m) or: {m < n} } (n isNumber?) <= (m isNumber?) = { (n == m) or: {n < m} } (n isNumber?) isPositive? = { n >= 0 } (n isNumber?) isNegative? = { n <= 0 } # ___________________________________________________________________________ # Tagged Values # > tv isTaggedWith: t # Returns .false if `tv` is not tagged with `t`. # Otherwise, returns `[v]` if `tv` is `(t taggedWith: v)` # Intended for use in match patterns: `(_ isTaggedWith:t = [v])` (asTagged:_ = [v, t]) isTaggedWith: (_ == t) = { [v] } (_ isTagged?) isTaggedWith: _ = { .false } # > tag: tv = t # Returns the tag `t` from the tagged value `tv` tag: (asTagged:_ = [v, t]) = { t } # > taggedValue: tv = v # Returns the value `v` from the tagged value `tv` taggedValue: (asTagged:_ = [v, t]) = { v } # ___________________________________________________________________________ # Functions # > apply:item to:args # Like `item(args)`, except that when typing `item(args)` explicitly, the parser # automatically makes a list containing whatever is in the parentheses. # If you wish to supply your own, constructed list, use `apply:item to:args`. apply:item to:args = { # Looks very magic, but ()(()) just names the function invoked by `x(y)`. # Ah, so many layers of indirection. (()(()))(item, args) } # ___________________________________________________________________________ # Standard Exceptions Exception = type:"Exception" superTypes:[] NotFound = type:"NotFound" superTypes:[Exception] NoMatchingCaseFound = type:"NoMatchingCaseFound" superTypes:[Exception] # ___________________________________________________________________________ # Associations, or key-value pairs Assoc = type:"Assoc" superTypes:[] (key) -> (value) = { Assoc(key, value) } (x as: Assoc = [key, _]) field: .key = {key} (x as: Assoc = [_, value]) field: .value = {value} repr:(a isA: Assoc) = { (repr: a.key) + (" -> " + (repr: a.value)) } # ___________________________________________________________________________ # Association Lists # > alist isNonEmptyAssocList? (hdtl: _ = [_ isA: Assoc, _]) isNonEmptyAssocList? = { .true } # > alist isAssocList? (_ isNonEmptyAssocList?) isAssocList? = { .true } [] isAssocList? = { .true } (_) isAssocList? = { .false } # > alist isDict? # Association lists are dictionaries. (l isAssocList?) isDict? = { .true } # > lookup:key in:alist # Returns [value] or [] if the key is not found. lookup:key in:(dict isNonEmptyAssocList?) = { [kv, tl] = hdtl: dict if: (kv.key == key) then: { [kv.value] } else: { lookup:key in:tl } } lookup:key in:[] = { [] } # > keys: alist keys: (alist isAssocList?) = { alist map: {:kv kv.key} } # > values: alist values: (alist isAssocList?) = { alist map: {:kv kv.value} } # > assocListMapping:keys to:values # Constructs a map with each item in `keys` mapped # to a corresponding item in `values`. assocListMapping:(keys isList?) to:(values isList?) = { (keys zip: values) map: {:[k,v] k -> v} } # > alist with:key mappedTo:value # Maps `key` to `value` in the assocation list `alist`. (alist isAssocList?) with:k mappedTo:v = { (k -> v) :: alist } # > alist without:key [] without:k = { [] } (hdtl:_ = [hd isA: Assoc, tl]) without:k = { if:(hd.key == k) then:{ tl without:k } else:{ hd :: (tl without: k) } } # ___________________________________________________________________________ # List Operations # > [] isEmptyList? = true [] isEmptyList? = { .true } _ isEmptyList? = { .false } # > isNonEmptyList? (lst isList?) isNonEmptyList? = { not: (lst isEmptyList?) } _ isNonEmptyList? = { .false } # > lst + lst [] + (l isList?) = { l } (l isList?) + [] = { l } (hdtl:_ = [hd, tl]) + (l isList?) = { hd :: (tl + l) } # > hdtl: lst = [hd, tl] # Break apart a list. hdtl: (lst isList?) = { [hd: lst, tl: lst] } # > length: lst length: [] = { 0 } length: (lst isList?) = { 1 + (length: (tl: lst)) } # > lst[index] # Returns a specific item in the list. As in Python, # negative numbers count from the end (so -1 is the last item). (lst isList?) [ [0] ] = { hd: lst } (lst isList?) [ [n isNegative?] ] = { lst[(length:lst) + n] } (lst isList?) [ [n isPositive?] ] = { (tl: lst)[n - 1] } # > enumerate: lst # Returns a list of `[index, item]` pairs where # `index` is the index of `item` in list. enumerate: (lst isList?) = { enumerate: lst from: 0 } # > enumerate: lst from: n # Like `enumerate:`, but starts from a specific value. enumerate: [] from: (i isNumber?) = { [] } enumerate: (hdtl: _ = [hd, tl]) from: (i isNumber?) = { [i, hd] :: (enumerate: tl from: (i+1)) } # > cond isTrueForAnyOf: lst (cond isCallable?) isTrueForAny:[] = { .false } (cond isCallable?) isTrueForAny:(hdtl:_ = [hd, tl]) = { (cond(hd)) or: {cond isTrueForAny: tl} } # > cond isTrueForAll: lst (cond isCallable?) isTrueForAll:[] = { .true } (cond isCallable?) isTrueForAll:(hdtl:_ = [hd, tl]) = { (cond(hd)) and: {cond isTrueForAll: tl} } # > first: lst where: cond # Returns `[item]` where `item` is the first # member of `lst` where `cond(item)` returns # non-false. Returns `[]` if there is no such # item. first: [] where: (cond isCallable?) = { [] } first: (lst isList?) where: (cond isCallable?) = { hd = hd:lst if: (cond(hd)) then: { [hd] } else: { first: (tl:lst) where: cond } } # > item in: lst # True if item is == to anything in lst. needle in: (haystack isList?) = { (_ == needle) isTrueForAny: haystack } # > forEach:lst do:func # Like map:, but executed purely for side-effects. Always # returns an empty list. forEach:[] do:(func isCallable?) = { [] } forEach:(hdtl:_ = [hd, tl]) do:(func isCallable?) = { func(hd) forEach:tl do:func } # > forEach:lst1 and:lst2 do:func # Invokes `func(i1, i2)` where `i1` and `i2` are corresponding # members of `lst1` and `lst2`. Always returns an empty list. forEach:[] and:(lst2 isList?) do:(func isCallable?) = { [] } forEach:(lst1 isList?) and:[] do:(func isCallable?) = { [] } forEach:(hdtl:_ = [hd1,tl1]) and:(hdtl:_ = [hd2, tl2]) do:(func isCallable?) = { func(hd1, hd2) forEach:tl1 and:tl2 do:func } # > lst map: func # Returns a new list `[ func(lst[0]) ... func(lst[N]) ]` [] map: (func isCallable?) = { [] } (lst isList?) map: (func isCallable?) = { [hd, tl] = hdtl: lst (func(hd)) :: (tl map: func) } # > lst1 and: lst2 map: func # Returns a new list `[ func(lst1[0],lst2[0]) ... func(lst1[N], lst2[N])]` # where `N` is the minimum of `length:lst1` and `length:lst2` [] and:(lst2 isList?) map: (func isCallable?) = { [] } (lst1 isList?) and:[] map: (func isCallable?) = { [] } (hdtl:_ = [hd1, tl1]) and:(hdtl:_ = [hd2, tl2]) map: (func isCallable?) = { (func(hd1, hd2)) :: (tl1 and: tl2 map: func) } # > lst field: fld # Maps `field:fld` across all items in the list. (l isList?) field: fld = { l map: (_ field: fld) } # > lst where: func # Returns a new list containing those items in `lst` where # `func(item)` returns non-false. [] where: (func isCallable?) = { [] } (hdtl:_ = [hd, tl]) where: (func isCallable?) = { if:(func(hd)) then: { hd :: (tl where: func) } else: { tl where: func } } # > lst zip: lst2 # Returns a list of lists [ ... [lst[i], lst2[i]] ... ]. # The list is as long as the shorter of lst and lst2. [] zip: (_ isList?) = { [] } (_ isList?) zip: [] = { [] } (lst isList?) zip: (lst2 isList?) = { [hd: lst, hd: lst2] :: ((tl: lst) zip: (tl: lst2)) } # ___________________________________________________________________________ # Symbols # > .foo(x) = x.foo = x field:.foo (sym isSymbol?) ( [arg] ) = { arg field: sym } # ___________________________________________________________________________ # Dictionaries # # Dictionaries define an interface. There are several implementations. # The standard library provide several implementations, such as # association lists. # # Dictionary interface: # # > dict isDict? # > lookup:key in:dict Yields [] if key is not found or [value] # > dict with:key mappedTo:value Yields a new dictionary # # In addition, the following operators are defined against # dictionaries by default: # # > dict{key} # > dict.key # > dict isDict? # If true, dict obeys the dictionary interface. d isDict? = { .false } # > dict[key] # > dict.key # "Syntactic sugar" for `lookup:key in:dict` (d isDict?) { [key] } = { match:[lookup:key in:d] with:[ { :[value] value } { :[] throw: (NotFound(key)) } ] } (d isDict?) field: fld = { d{fld} } # > alist hasKey: key (alist isDict?) hasKey:key = { match:[lookup:key in:alist] with:[ { :[] .false } { :[_] .true } ] } # ___________________________________________________________________________ # Types # # Built-in support for a nominal type-system with arbitrary subtyping. # Currently, it is the user's responsibility not to create a cyclic # type hierarchy! Should fix that. # # By convention, type variables are named with uppercase letters. # > type:name superTypes:[] # Constructs a new type type:aName superTypes:aSuperTypes = { [aName, aSuperTypes] taggedWith: .MetaType } # > t isType? # Returns `.true` if `t` is a user-defined type (_ isTaggedWith: .MetaType) isType? = { .true } _ isType? = { .false } # > typeData: t1 = [name, superTypes] typeData: (_ isTaggedWith: .MetaType = [typeData]) = { typeData } typeData: (.MetaType) = { ["MetaType", []] } # > t1 isSubtypeOf: t2 # Tests the subtype relation (sub isType?) isSubtypeOf: (sup isType?) = { if: (sub == sup) then: { .true } else: { [_, subSupTypes] = typeData: sub (_ isSubtypeOf: sup) isTrueForAny: subSupTypes } } # ___________________________________________________________________________ # Instances # # Instances are just data tagged with some type. # > t(data) # Constructs an instance of type `t` with instance data `data`. (aType isType?) ( argData ) = { argData taggedWith:aType } # > x isInstance? # Is this an instance of some type? (asTagged:_ = [_, _ isType?]) isInstance? = { .true } # > asInstance:x = [t, d] # If `x` is an instance, returns a list with the type `t` that # `x` is an instance of and the instance data `d`. asInstance:(asTagged:_ = [d, t isType?]) = { [t, d] } # > x isA: T # tests if `x` is of type `T`, returning `.true` or `.false` (asInstance:_ = [objType, _]) isA: (someType isType?) = { objType isSubtypeOf: someType } # > x as: T # returns the data for `x` if it is of type `T`, else `.false` (asInstance:_ = [objType, objData]) as: (someType isType?) = { if: (objType isSubtypeOf: someType) then: { objData } else: { .false } } # > repr:instance # Default pretty-prints an instance as string. repr:(asInstance:_ = [objType, objData]) = { [typeName, _] = typeData: objType typeName + ("(" + ((repr: objData) + ")")) # yuck :) } # > obj field: fld # loads the field with given name assuming objData is some dict (asInstance:_ = [_, data isDict?]) field: fld = { data{fld} } # ___________________________________________________________________________ # Callable Interface # # The isCallable? test checks for whether `x(...)` is a well-defined operation # for some type x. # > x isCallable? # True if `x(...)` is well-defined. Users can and should extend this with cases # of their own. (f isFunc?) isCallable? = { .true } (t isType?) isCallable? = { .true } (s isSymbol?) isCallable? = { .true }