diff --git a/src/Lists/Lists.jl b/src/Lists/Lists.jl index 4a528fc..0f9acdb 100644 --- a/src/Lists/Lists.jl +++ b/src/Lists/Lists.jl @@ -1,4 +1,10 @@ -@reexport module Lists +""" +Provides a dynamically typed singly-linked list implementation, `List` similar to that +available in most lisp dialects. This module provides basic Julia functionality to +manipulate such lists, exported under both the Julia and Scheme names where they differ +(such as `Base.first` vs. `car`. +""" +module Lists import FunctionalCollections: append using Base.Iterators @@ -8,9 +14,25 @@ const car = Base.first export Cons, List, isnil, ispair, car, cdr, caar, cadr, cddr, nil, append, ++, cons, list, islist, Nil +""" +An object representing nothing, or an empty list. + +The singleton instance of this type is called `nil`. This type is isomorphic to, and very +similar, to `Nothing`. However, it is often useful to distinguish the `nil` used within many +lisp dialects from a true `Nothing`, because `nil` is iterable and represents an empty list. +""" struct Nil end const nil = Nil.instance +""" +An object which is essentially a pair of two objects. + +The two objects are referred to, for historical reasons, as `car` and `cdr`.These +abbreviations are not semantically relevant today, so can generally be thought of as the +head object and the tail object. For `Cons` objects which are (proper) lists, the `car` +(head) object will be the first element of the list, and the `cdr` (tail) object will be a +list representing the remaining elements. +""" struct Cons car cdr @@ -18,6 +40,11 @@ end const cons = Cons car(α::Cons) = α.car +""" +The tail of a `List`. + +Note that the tail need not be a list (or even a `List`) if the list is improper. +""" cdr(α::Cons) = α.cdr caar(α::Cons) = car(car(α)) cadr(α::Cons) = car(cdr(α)) @@ -34,11 +61,12 @@ end Base.:(==)(α::Cons, β::Cons) = car(α) == car(β) && cdr(α) == cdr(β) """ -An immutable linked list. +An immutable singly linked list, which may be improper. -A `List` is either `Nil` (empty) or `Cons`, where the first element is an arbitrary object -and the second element is a `List`. Note that this second requirement is not enforced by the -type system, since improper lists are allowable in many lisp dialects. +A list is either `Nil` (empty) or `Cons`, where the first element is an arbitrary object and +the second element is a list. The `List` type, however, includes all instances of `Cons`, +including those for which the second element is not a list. Such `List` instances are called +improper lists, which are allowed because they are used in many lisp dialects. """ const List = Union{Cons, Nil} @@ -51,6 +79,12 @@ isnil(::Cons) = false ispair(α::List) = !isnil(α) +""" +Return `true` if the provided `List` is in fact a list, i.e., it is not improper. + +This is the case if the list is a `Nil` (empty list) or a `Cons` with a proper list as its +tail. +""" islist(::Nil) = true islist(α::Cons) = islist(cdr(α)) diff --git a/src/Lists/show.jl b/src/Lists/show.jl index 34e17f6..fa84f4c 100644 --- a/src/Lists/show.jl +++ b/src/Lists/show.jl @@ -10,6 +10,10 @@ =# # TODO: missing lots of native s-expression types here. also non-native types +# TODO: improper list printing +""" +Convenience function to produce compact strings from (proper) lists of lisp objects. +""" unparse(α::List) = "(" * join(unparse.(α), " ") * ")" unparse(b::Bool) = b ? "#t" : "#f" unparse(s::Symbol) = string(s) @@ -28,12 +32,8 @@ space(ctx::ShowListContext) = max(5, ctx.limit - ctx.indent) # performance is really not our concern sindent(ctx::ShowListContext) = " " ^ ctx.indent -# “if we append or prepend, we should indent, not indented” -# no. indent is also a noun. indented makes it clear what this does indented(ctx::ShowListContext, i=2) = ShowListContext(ctx.indent + i, ctx.limit) -# putting an ‘s’ in front of all functions that return strings is terrible style -# but sometimes terrible style is the best we’ve got sprintwidth(α) = sum(charwidth(c) for c in unparse(α)) spprintall(ctx::ShowListContext, α) = join((β -> spprint(ctx, β)) ∘ α, '\n') diff --git a/src/Parser/Parser.jl b/src/Parser/Parser.jl index ae2dd8c..015bd2b 100644 --- a/src/Parser/Parser.jl +++ b/src/Parser/Parser.jl @@ -309,7 +309,8 @@ end """ parse(s::AbstractString) -Read the given string `s` as a single s-expression. +Read the given string `s` as a single s-expression. The alternative vocabulary +`Base.parse(SExpression, s)` is also supported and may be preferred. """ function parse(s::AbstractString) buf = IOBuffer(s) @@ -337,6 +338,9 @@ parse(io::IO) = let x = nextobject(io) end end +# allow `parse(SExpression, o)`, which reads nicer than `SExpression.parse(o)` +Base.parse(::Type{SExpression}, o) = parse(o) :: SExpression + """ parsefile(filename::AbstractString) diff --git a/src/SExpressions.jl b/src/SExpressions.jl index 5c270df..748cdc5 100644 --- a/src/SExpressions.jl +++ b/src/SExpressions.jl @@ -9,7 +9,7 @@ include("Parser/Parser.jl") export SExpression import .Parser: parse, parseall, parsefile, SExpression -using .Lists +@reexport using .Lists macro sx_str(x::String) QuoteNode(parse(x)) diff --git a/test/parser.jl b/test/parser.jl index ca83f9e..402974a 100644 --- a/test/parser.jl +++ b/test/parser.jl @@ -100,4 +100,8 @@ end @test @eval(sx"foo") == :foo end +@testset "interface" begin + @test parse(SExpression, "(1 . + . (2 . * . 3))") == List(:+, 1, List(:*, 2, 3)) +end + end