Skip to content

Commit

Permalink
Added docs. Made constructors work better.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonniedie committed Oct 2, 2020
1 parent 1f0c605 commit d060a20
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 34 deletions.
19 changes: 13 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,26 @@ os:
- linux
- osx
- windows
arch:
- x64
cache:
directories:
- ~/.julia/artifacts
jobs:
fast_finish: true
allow_failures:
- julia: nightly
after_success:
- |
julia -e '
using Pkg
Pkg.add("Coverage")
using Coverage
Codecov.submit(process_folder())'
jobs:
fast_finish: true
allow_failures:
- julia: nightly
include:
- stage: Documentation
julia: 1.5
script: julia --project=docs -e '
using Pkg;
Pkg.develop(PackageSpec(path=pwd()));
Pkg.instantiate();
include("docs/make.jl");'
after_success: skip
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "ConcreteStructs"
uuid = "2569d6c7-a4a2-43d3-a901-331e8e4be471"
authors = ["Jonnie Diegelman <[email protected]> and contributors"]
version = "0.1.0"
version = "0.1.1"

[compat]
julia = "1.1"
Expand Down
2 changes: 2 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
site/
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
25 changes: 25 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Documenter
using ConcreteStructs

makedocs(
modules = [ConcreteStructs],
sitename = "ConcreteStructs.jl",
pages =[
"Home" => "index.md",
"Walkthrough" => "walkthrough.md",
"API" => "api.md",
],
format = Documenter.HTML(
canonical = "https://jonniedie.github.io/ConcreteStructs.jl/stable",
),
repo="https://github.com/jonniedie/ConcreteStructs.jl/blob/{commit}{path}#L{line}",
authors = "Jonnie Diegelman",
assets = String[],
)

# Documenter can also automatically deploy documentation to gh-pages.
# See "Hosting Documentation" and deploydocs() in the Documenter manual
# for more information.
deploydocs(
repo = "github.com/jonniedie/ConcreteStructs.jl.git"
)
26 changes: 26 additions & 0 deletions docs/make_local.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Documenter
using ConcreteStructs

makedocs(
modules = [ConcreteStructs],
sitename = "ConcreteStructs.jl",
pages =[
"Home" => "index.md",
"Walkthrough" => "walkthrough.md",
"API" => "api.md",
],
format = Documenter.HTML(
canonical = "https://jonniedie.github.io/ConcreteStructs.jl/stable",
prettyurls=false,
),
repo="https://github.com/jonniedie/ConcreteStructs.jl/blob/{commit}{path}#L{line}",
authors = "Jonnie Diegelman",
assets = String[],
)

# Documenter can also automatically deploy documentation to gh-pages.
# See "Hosting Documentation" and deploydocs() in the Documenter manual
# for more information.
deploydocs(
repo = "github.com/jonniedie/ConcreteStructs.jl.git"
)
3 changes: 3 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```@docs
@concrete
```
6 changes: 6 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# ConcreteStructs.jl

ConcreteStructs exports the macro `@concrete` that will add type parameters to your struct for any field where type parameters aren’t given.

Simply add the `@concrete` macro before any valid `struct` definition and it should automagically make all of your non-type-annotated fields type-annotated. If you don't like the verbose type printing

74 changes: 74 additions & 0 deletions docs/src/walkthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Walkthrough
The `@concrete` macro can be tacked onto any `struct` definition that isn't already concretely-typed. That's kinda all there is to it, but let's walk through some examples anyway to get a feel for it.

In this example, no type parameters are given, so they will be filled in automatically:

```julia
@concrete struct Whatever
a
b
end
```
```julia-repl
julia> complex_whatever = Whatever(1+im, "It's pretty complex")
Whatever{Complex{Int64},String}(1 + 1im, "It's pretty complex")
```

But maybe we don't want to show the type parameters, since we never cared much about them in the first place. If we want our `struct` to print a little more succintly, we can add the `terse` keyword. Now it will print as if we never added the `@concrete` macro.

```julia
@concrete terse struct PrettierWhatever
a
b
end
```
```julia-repl
julia> pretty_whatever = PrettierWhatever(1+im, "It's still pretty complex")
PrettierWhatever(1 + 1im, "It's still pretty complex")
```
The full type information is still available for inspection with the `typeof` function, though.
```julia-repl
julia> typeof(pretty_whatever)
PrettierWhatever{Complex{Int64},String}
```

More complicated type parameterizations are possible as well. Take this example of an array with two metadata fields attached. The type parameters for the `array` field are provided but the `name` field's type is left open. The `@concrete` macro will respect the given type parameters and concretely parameterize the `name` field.

```julia
@concrete struct MetaArray{T,N,A<:AbstractArray{T,N}} <: AbstractArray{T,N}
array::A
name
end

Base.size(x::MetaArray) = size(x.array)

Base.getindex(x::MetaArray, i...) = getindex(x.array[i...])
```
```julia-repl
julia> abed = MetaArray([8,10,2,5], "Abed")
4-element MetaArray{Int64,1,Array{Int64,1},String}:
8
10
2
5
```

We can also have type parameters that don't correspond to any field. In this example, the `BananaStand` type is parameterized by the boolean value `has_money`.

```julia
@concrete terse mutable struct BananaStand{has_money}
employees
manager
end
```

In this case, the constructor must be given with the `has_money` parameter, just like it would need to be if we weren't using the `@concrete` macro. Since the `terse` keyword was give, the type will print exactly as it's specified: with the `has_money` parameterization but no field parameterizations.

```julia-repl
julia> the_banana_stand = BananaStand{true}(["Maeby", "Annyong"], "George Michael")
BananaStand{true}(["Maeby", "Annyong"], "George Michael")
julia> typeof(the_banana_stand)
BananaStand{true,Array{String,1},String}
```

104 changes: 89 additions & 15 deletions src/ConcreteStructs.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""
ConcreteStructs
ConcreteStructs exports the macro `@concrete`, which can be used to make non-concrete structs
concrete.
ConcreteStructs exports the macro `@concrete`, which can be used to concretely parameterize
structs.
"""
module ConcreteStructs

Expand Down Expand Up @@ -63,41 +63,115 @@ FGH{Int64,1,Array{Int64,1},Nothing}
```
"""
macro concrete(expr)
return _make_concrete(expr) |> esc
expr, _ = _concretize(expr)

return quote
$expr
end |> esc
end

macro concrete(terse, expr)
terse isa Symbol && terse == :terse || error("Invalid usage of @concrete")
expr = _make_concrete(expr)
struct_name = expr.args[2].args[1].args[1]
full_params = "{" * join(expr.args[2].args[1].args[2:end], ",") * "}"
expr, type_params = _concretize(expr)
struct_name = expr.args[2].args[1].args[1] |> string

num_params = length(type_params)
terse_string = if num_params == 0
struct_name
else
:($(struct_name) * "{" * join(T.parameters[1:$num_params], ",") * "}")
end

return quote
$expr
Base.show(io::IO, ::Type{<:$struct_name}) = print(io, $(string(struct_name)))
function Base.show(io::IO, ::MIME"text/plain", T::Type{<:$struct_name})
return print(io, $(string(struct_name)) * "{" * join(T.parameters, ",") * "}")
function Base.show(io::IO, T::Type{<:$(Symbol(struct_name))})
return print(io, $terse_string)
end
function Base.show(io::IO, ::MIME"text/plain", T::Type{<:$(Symbol(struct_name))})
return print(io, $(struct_name) * "{" * join(T.parameters, ",") * "}")
end
end |> esc
end


# Parse whole struct definition for the @concrete macro
function _make_concrete(expr)
function _concretize(expr)
expr isa Expr && expr.head == :struct || error("Invalid usage of @concrete")

maybe_mutable = expr.args[1]
(struct_name, type_params, super) = _parse_head(expr.args[2])
struct_name, type_params, super = _parse_head(expr.args[2])
line_tuples = _parse_line.(expr.args[3].args)

lines = first.(line_tuples)
type_params = (type_params..., filter(x -> x!==nothing, last.(line_tuples))...)
struct_type = Expr(:curly, struct_name, type_params...)
type_params_full = (type_params..., filter(x -> x!==nothing, last.(line_tuples))...)
struct_type = if length(type_params_full) == 0
struct_name
else
Expr(:curly, struct_name, type_params_full...)
end

head = Expr(:(<:), struct_type, super)
body = Expr(:block, lines...)

return Expr(:struct, maybe_mutable, head, body)
constructor_expr = _make_constructor(struct_name, type_params, type_params_full, lines)
body = Expr(:block, lines..., constructor_expr)

struct_expr = Expr(:struct, maybe_mutable, head, body)

return struct_expr, type_params
end


# Make the inner constructor function
function _make_constructor(struct_name, type_params, type_params_full, lines)
field_lines = filter(line -> (!(line isa LineNumberNode) && (line.head === :(::))), lines)
args = map(x->x.args, field_lines)
vars = first.(args)
var_types = last.(args)
constructor_params = _get_constructor_params(type_params, var_types)
new_params = _strip_super(type_params_full)

if length(type_params) == length(type_params_full) && all(type_params .== type_params_full)
return Expr(:block)
elseif length(constructor_params)==0
return :(
function $struct_name($(field_lines...)) where {$(type_params_full...)}
return new{$(new_params...)}($(vars...))
end
)
else
return :(
function $struct_name{$(constructor_params...)}($(field_lines...)) where {$(type_params_full...)}
return new{$(new_params...)}($(vars...))
end
)
end
end


# Get the parameters that are unmatched to variables and need to be annoted in the constructor
function _get_constructor_params(type_params, var_types)
subparams = _get_subparams(type_params)
type_params = _strip_super(type_params)
var_types = [subparams; _strip_super(var_types)]
return setdiff(type_params, var_types)
end


# Strip supertype annotations
_strip_super(x) = x
_strip_super(x::Union{Tuple, AbstractVector}) = vcat(_strip_super.(x)...)
_strip_super(x::Expr) = x.head == :(<:) ? x.args[1] : x


# Get the subparameters of supertypes of subtype parameters (sorry)
_get_subparams(x) = []
_get_subparams(x::Union{Tuple, AbstractVector}) = vcat(_get_subparams.(x)...)
function _get_subparams(x::Expr)
if x.head === :curly
return x.args[2:end]
elseif x.head === :(<:)
return _get_subparams(x.args[2:end])
end
end


Expand Down
Loading

2 comments on commit d060a20

@jonniedie
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/22344

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.1 -m "<description of version>" d060a2078695b65f2a65d335a71a01bedd8b75d7
git push origin v0.1.1

Please sign in to comment.