Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,40 @@

[![Build Status](https://github.com/CarloLucibello/AutoStructs.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/CarloLucibello/AutoStructs.jl/actions/workflows/CI.yml?query=branch%3Amain)

This package provides the macro `@structdef` to automatically define a struct with a given name and fields starting from its constructor.
This package provides the macro `@structdef` to automatically define a struct with a given name and fields starting from its constructor.

The package has two goals:
- Combine the definition of a struct and its constructor in a single concise step.
- Allow to redefine a struct without restarting the REPL.
- Combine the definition of a struct and its constructor in a single concise step.

# Usage

The macro has two methods. The first just defines a struct, given the name & fields:

```julia
julia> using AutoStructs

# Define a struct from its fields:
julia> @structdef Store(some::Vector, things::Vector)
var"##Store#561"

# Re-define it:
julia> @structdef Store(some::AbstractVector, things)
var"##Store#562"

# Create an instance:
julia> obj = Store([1,2], 3)
Store(some = [1, 2], things = 3)

julia> obj isa Store
true

# Define methods
julia> Base.isempty(s::Store) = isempty(s.some) && isempty(s.things)
```

The second "method" of the macro acts on a constructor function:

```julia
julia> using AutoStructs, Random

Expand All @@ -21,6 +48,9 @@ julia> @structdef function Layer(din::Int, dout::Int)
end
var"##Layer#230"

# Make it callable:
julia> (lay::Layer)(x) = lay.weight * x .+ lay.bias

# Now we can create an instance of the struct
julia> layer = Layer(2, 4)
Layer(weight = [-1.422950752982445 1.6242590842562115; -1.3393896739857631 0.8191382347282851; 0.3944420481119003 0.5955417101440335; 1.3944705999832914 1.1224997165166155], bias = [0.0, 0.0, 0.0, 0.0])
Expand All @@ -34,6 +64,13 @@ var"##Layer#230"{Matrix{Float64}, Vector{Float64}}
julia> layer isa Layer{Matrix{Float64}, Vector{Float64}} # Layer is a parametric type
true

julia> @inferred layer([3,4.]) # ... hence operations using it are type-stable
4-element Vector{Float64}:
-0.1318661204117887
5.059105943934839
-1.0380824711450396
-1.9023758645557405

# You can redefine the struct without restarting the REPL
julia> @structdef function Layer(din::Int, dout::Int, activation = identity)
weight = randn(dout, din)
Expand Down Expand Up @@ -80,7 +117,7 @@ help?> @structdef
bias = zeros(dout)
return Layer(weight, bias)
end

layer = Layer(2, 4)
layer isa Layer # true

Expand All @@ -90,17 +127,17 @@ help?> @structdef
weight::T1
bias::T2
end

function Layer001(din::Int, dout::Int)
weight = randn(dout, din)
bias = zeros(dout)
return Layer001(weight, bias)
end

Layer = Layer001

Base.show(io::IO, x::Layer) = ... # we do some pretty printing
Base.show(io::IO, ::MIME"text/plain", x::Layer) = ...
Base.show(io::IO, ::MIME"text/plain", x::Layer) = ...

Since Layer001{T1, T2} can hold any objects, even Layer("hello", "world"), there should never
be an ambiguity between the struct's own constructor, and your constructor function. If the two
Expand All @@ -112,7 +149,7 @@ help?> @structdef
bias = zeros(dout)
return Layer(weight::AbstractMatrix, bias::AbstractVector)
end

layer = Layer(2, 4)

This creates a struct like this:
Expand Down
72 changes: 62 additions & 10 deletions src/AutoStructs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,45 @@ module AutoStructs

export @structdef

"""
@structdef MyType(field1, field2, ...)
@structdef MyType(field1::AbstractA, field2::AbstractB, ...)

This is a macro for easily defining new types. Used like this, the macro
creates a `struct` with the fields indicated.
`@structdef MyType(field1, field2::AbstractB)` expands to roughly this:

```julia
struct MyType001{T1, T2 <: AbstractB}
field1::T1
fiedl2::T2
end

MyType = MyType001 # this allows re-definition
```

If the macro is run again with different fields, or different types,
then another `struct MyType002` is created, bound to `MyType = MyType002`.
This allows re-definition without re-starting Julia.

The `struct`'s fields always have type parameters,
which are restricted `T1 <: AbstractA` if desired.
Thus operations on an instance of `m::MyType` should be type-stable,
but construction `m = MyType(x, y)` will not be.

You can make instances callable as usual,
and define methods sepcialising on the type:
```julia
(m::MyType)(x) = m.field1(x) / m.field2(x)

Base.length(m::MyType) = length(m.field1)
```
"""
macro structdef(ex)
esc(_structdef(ex))
end


"""
@structdef function MyType(args...; kws...); ...; return MyType(f1, f2, ...); end

Expand All @@ -13,13 +52,13 @@ Typically the steps to define a new `struct` are:
which initialises the fields.

This macro combines these steps into one, by taking the constructor definition
of step 2 and using the return line to automatically infer the field names
of step 2 and using the return line to automatically infer the field names
and define the `struct`.

Moreover, if you change the name or types of the fields,
Moreover, if you change the name or types of the fields,
then the `struct` definition is automatically replaced.
This works because this definition uses an auto-generated name, which is `== MyType`.
Thanks to this, you can easily experiment with different field names and types,
Thanks to this, you can easily experiment with different field names and types,
overcoming a major limitation of a Revise.jl workflow.

## Examples
Expand Down Expand Up @@ -52,11 +91,11 @@ end
Layer = Layer001

Base.show(io::IO, x::Layer) = ... # we do some pretty printing
Base.show(io::IO, ::MIME"text/plain", x::Layer) = ...
Base.show(io::IO, ::MIME"text/plain", x::Layer) = ...
```

Since `Layer001{T1, T2}` can hold any objects, even `Layer("hello", "world")`,
there should never be an ambiguity between the `struct`'s own constructor,
Since `Layer001{T1, T2}` can hold any objects, even `Layer("hello", "world")`,
there should never be an ambiguity between the `struct`'s own constructor,
and your constructor function. If the two have the same number of arguments,
you can avoid the ambiguity by using type restrictions in the input arguments
(as in the example above) or in the return line:
Expand All @@ -81,13 +120,26 @@ end
```
and reassigns `Layer = Layer002`.
"""
macro structdef(ex)
esc(_structdef(ex))
end
var"@structdef"

const DEFINE = Dict{UInt, Tuple}()

struct _NoCall
_NoCall() = error("this object is meant never to be created")
end

function _structdef(expr)
if Meta.isexpr(expr, :function) # original path, @structdef function MyStruct(...); ...
elseif Meta.isexpr(expr, :call) # one-line, @structdef MyStruct(field)
fun = expr.args[1]
newex = :(function $fun(_::$_NoCall) # perhaps not the cleanest implementation
$expr
end)
return _structdef(newex)
else
throw("Expected a function definition, like `@structdef function MyStruct(...); ...`, or a call like `@structdef MyStruct(...)`")
end

# Check first & last line of the input expression:
Meta.isexpr(expr, :function) || throw("Expected a function definition, like `@structdef function MyStruct(...); ...`")
fun = expr.args[1].args[1]
Expand Down Expand Up @@ -118,7 +170,7 @@ function _structdef(expr)
Expr(:(<:), ft.args[2], ex.args[2])
end
end

strfun = "$fun"
ex = quote
struct $name{$(types...)}
Expand Down