Construct complex structures within single call + simple compile-time meta-inheritance model with mixins.

Overview

Introduction

constructivism is a Rust sample-library designed to simplify the construction of structured data by defining and manipulating sequences of Constructs. This README provides an overview of how to use constructivism and how it can be inlined into you project using constructivist library.

Installation

To use Constructivism in your Rust project, add it as a dependency in your Cargo.toml file:

[dependencies]
constructivism = "0.0.2"

Or let the cargo do the stuff:

cargo add constructivism

Constructivism can be inlined into you library as for example your_library_constructivism within constructivist crate. See instructions.

Guide

See also examples/tutorial.rs

Getting Started

Usually you start with

use constructivism::*;

Constructs and Sequences

1.1. Constructs: Constructivism revolves around the concept of Constructs. You can derive construct like this:

#[derive(Construct)]
pub struct Node {
    hidden: bool,
    position: (f32, f32),
}

1.2 construct!: You can use the construct! macro to create instances of Constructs. Please note the dots at the beginning of the each param, they are required and you will find this syntax quite useful.

fn create_node() {
    let node = construct!(Node {
        .position: (10., 10.),
        .hidden: true
    });
    assert_eq!(node.position.0, 10.);
    assert_eq!(node.hidden, true);
}

1.3 Sequences: A Construct can be declared only in front of another Construct. constructivism comes with only Nothing, () construct. The Self -> Base relation called Sequence in constructivism. You can omit the Sequence declaration, Self -> Nothing used in this case. If you want to derive Construct on the top of another meaningful Construct, you have to specify Sequence directly with #[construct(/* Sequence */)] attribute.

#[derive(Construct)]
#[construct(Rect -> Node)]
pub struct Rect {
    size: (f32, f32),
}

1.4 Constructing Sequences: The Sequence for the Rect in example above becomes Rect -> Node -> Nothing. You can construct! the entire sequence within a single call:

fn create_sequence() {
    let (rect, node /* nothing */) = construct!(Rect {
        .hidden,                        // You can write just `.hidden` instead of `.hidden: true`
        .position: (10., 10.),
        .size: (10., 10.),
    });
    assert_eq!(rect.size.0, 10.);
    assert_eq!(node.position.1, 10.);
    assert_eq!(node.hidden, false);
}

1.5 Params: There are different kind of Params (the things you passing to construct!(..)):

  • Common: use Default::default() if not passed to construct!(..)
  • Default: use provided value if not passed to construct!(..)
  • Required: must be passed to construct!(..)
  • Skip: can't be passed to construct!(..), use Default::default() or provided value You configure behavior using #[param] attribute when deriving:
#[derive(Construct)]
#[construct(Follow -> Node)]
pub struct Follow {
    offset: (f32, f32),                 // Common, no #[param]

    #[param(required)]                  // Required
    target: Entity,
    
    #[param(default = Anchor::Center)]  // Default
    anchor: Anchor,

    #[param(skip)]                      // Skip with Default::default()
    last_computed_distance: f32,

    #[param(skip = FollowState::None)]  // Skip with provided value
    state: FollowState,
}

#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Entity;

pub enum Anchor {
    Left,
    Center,
    Right,
}

pub enum FollowState {
    None,
    Initialized(f32)
}

1.6 Passing params: When passing params to construct!(..) you have to pass all required for Sequence params, or you will get the compilation error. You can omit non-required params.

fn create_elements() {
    // omit everything, default param values will be used
    let (rect, node, /* nothing */) = construct!(Rect);
    assert_eq!(node.hidden, false);
    assert_eq!(rect.size.0, 0.);

    // you have to pass target to Follow, the rest can be omitted..
    let (follow, node) = construct!(Follow {
        .target: Entity
    });
    assert_eq!(follow.offset.0, 0.);
    assert_eq!(node.hidden, false);

    // ..or specified:
    let (follow, node) = construct!(Follow {
        .hidden,
        .target: Entity,
        .offset: (10., 10.),

        // last_computed_distance param is skipped, uncommenting
        // the next line will result in compilation error
        // error: no field `last_computed_distance` on type `&follow_construct::Params`
        
        // .last_computed_distance: 10.
    });
    assert_eq!(follow.offset.0, 10.);
    assert_eq!(node.hidden, true);
}

Design and Methods

2.1 Designs and Methods: Every Construct has its own Design. You can implement methods for a Construct's design:

impl NodeDesign {
    pub fn move_to(&self, entity: Entity, position: (f32, f32)) { }
}

impl RectDesign {
    pub fn expand_to(&self, entity: Entity, size: (f32, f32)) { }
}

2.2 Calling Methods: You can call methods on a Construct's design. Method resolution follows the sequence order:

fn use_design() {
    let rect_entity = Entity;
    design!(Rect).expand_to(rect_entity, (10., 10.));
    design!(Rect).move_to(rect_entity, (10., 10.)); // move_to implemented for NodeDesign
}

Segments

3.1 Segments: Segments allow you to define and insert segments into a Construct's sequence:

#[derive(Segment)]
pub struct Input {
    disabled: bool,
}

#[derive(Construct)]
#[construct(Button -> Input -> Rect)]
pub struct Button {
    pressed: bool
}

3.2 Sequence with Segments: The Sequence for Button becomes Button -> Input -> Rect -> Node -> Nothing. You can instance the entire sequence of a Construct containing segments within a single construct! call:

fn create_button() {
    let (button, input, rect, node) = construct!(Button {
        .disabled: true
    });
    assert_eq!(button.pressed, false);
    assert_eq!(input.disabled, true);
    assert_eq!(rect.size.0, 100.);
    assert_eq!(node.position.0, 0.);
}

3.3 Segment Design: Segment has its own Design as well. And the method call resolves within the Sequence order as well. Segment's designs has one generic parameter - the next segment/construct, so you have to respect it when implement Segment's Design:

impl<T> InputDesign<T> {
    fn focus(&self, entity: Entity) {
        /* do the focus stuff */
    }
}

fn focus_button() {
    let btn = Entity;
    design!(Button).focus(btn);
}

Props

4.1 Props: By deriving Constructs or Segments you also get the ability to set and get properties on items with respect of Sequence:

fn button_props() {
    let (mut button, mut input, mut rect, mut node) = construct!(Button);
    
    // You can access to props knowing only the top-level Construct
    let pos         /* Prop<Node, (f32, f32)> */    = prop!(Button.position);
    let size        /* Prop<Rect, (f32, f32)> */    = prop!(Button.size);
    let disabled    /* Prop<Input, bool> */         = prop!(Button.disabled);
    let pressed     /* Prop<Button, bool */         = prop!(Button.pressed);

    // You can read props. You have to pass exact item to the get()
    let x = pos.get(&node).as_ref().0;
    let w = size.get(&rect).as_ref().0;
    let is_disabled = *disabled.get(&input).as_ref();
    let is_pressed = *pressed.get(&button).as_ref();
    assert_eq!(0., x);
    assert_eq!(100., w);
    assert_eq!(false, is_disabled);
    assert_eq!(false, is_pressed);

    // You can set props. You have to pass exact item to set()
    pos.set(&mut node, (1., 1.));
    size.set(&mut rect, (10., 10.));
    disabled.set(&mut input, true);
    pressed.set(&mut button, true);
    assert_eq!(node.position.0, 1.);
    assert_eq!(rect.size.0, 10.);
    assert_eq!(input.disabled, true);
    assert_eq!(button.pressed, true);

}

4.2 Expand props: If you have field with Construct type, you can access this fields props as well:

#[derive(Construct, Default)]
#[construct(Vec2 -> Nothing)]
pub struct Vec2 {
    x: f32,
    y: f32,
}

#[derive(Construct)]
#[construct(Node2d -> Nothing)] 
pub struct Node2d {
    #[prop(construct)]      // You have to mark expandable props with #[prop(construct)]
    position: Vec2,
}

fn modify_position_x() {
    let mut node = construct!(Node2d);
    assert_eq!(node.position.x, 0.);
    assert_eq!(node.position.y, 0.);

    let x = prop!(Node2d.position.x);

    x.set(&mut node, 100.);
    assert_eq!(node.position.x, 100.);
    assert_eq!(node.position.y, 0.);
}

Custom Constructors

5.1 Custom Constructors: Sometimes you may want to implement Construct for a foreign type or provide a custom constructor. You can use derive_construct! for this purpose:

pub struct ProgressBar {
    min: f32,
    val: f32,
    max: f32,
}


impl ProgressBar {
    pub fn min(&self) -> f32 {
        self.min
    }
    pub fn set_min(&mut self, min: f32) {
        self.min = min;
        if self.max < min {
            self.max = min;
        }
        if self.val < min {
            self.val = min;
        }
    }
    pub fn max(&self) -> f32 {
        self.max
    }
    pub fn set_max(&mut self, max: f32) {
        self.max = max;
        if self.min > max {
            self.min = max;
        }
        if self.val > max {
            self.val = max;
        }
    }
    pub fn val(&self) -> f32 {
        self.val
    }
    pub fn set_val(&mut self, val: f32) {
        self.val = val.max(self.min).min(self.max)
    }
}

derive_construct! {
    // Sequence
    seq => ProgressBar -> Rect;

    // Constructor, all params with default values
    construct => (min: f32 = 0., max: f32 = 1., val: f32 = 0.) -> {
        if max < min {
            max = min;
        }
        val = val.min(max).max(min);
        Self { min, val, max }
    };

    // Props using getters and setters
    props => {
        min: f32 = [min, set_min];
        max: f32 = [max, set_max];
        val: f32 = [val, set_val];
    };
}

5.2 Using Custom Constructors: The provided constructor will be called when creating instances:

fn create_progress_bar() {
    let (pb, _, _) = construct!(ProgressBar { .val: 100. });
    assert_eq!(pb.min, 0.);
    assert_eq!(pb.max, 1.);
    assert_eq!(pb.val, 1.);
}

5.3 Custom Construct Props: In the example above derive_construct! declares props using getters and setters. This setters and getters are called when you use Prop::get and Prop::set

fn modify_progress_bar() {
    let (mut pb, _, _) = construct!(ProgressBar {});
    let min = prop!(ProgressBar.min);
    let val = prop!(ProgressBar.val);
    let max = prop!(ProgressBar.max);

    assert_eq!(pb.val, 0.);

    val.set(&mut pb, 2.);
    assert_eq!(pb.val, 1.0); //because default for max = 1.0

    min.set(&mut pb, 5.);
    max.set(&mut pb, 10.);
    assert_eq!(pb.min, 5.);
    assert_eq!(pb.val, 5.);
    assert_eq!(pb.max, 10.);
}

5.4 Deriving Segments: You can derive Segments in a similar way:

pub struct Range {
    min: f32,
    max: f32,
    val: f32,
}
derive_segment! {
    // use `seg` to provide type you want to derive Segment
    seg => Range;
    construct => (min: f32 = 0., max: f32 = 1., val: f32 = 0.) -> {
        if max < min {
            max = min;
    }
        val = val.min(max).max(min);
        Self { min, val, max }
    };
    // Props using fields directly
    props => {
        min: f32 = value;
        max: f32 = value;
        val: f32 = value;
    };
}

#[derive(Construct)]
#[construct(Slider -> Range -> Rect)]
pub struct Slider;

fn create_slider() {
    let (slider, range, _, _) = construct!(Slider {
        .val: 10.
    });
    assert_eq!(range.min, 0.0);
    assert_eq!(range.max, 1.0);
    assert_eq!(range.val, 1.0);
}

Limitations

  • only public structs (or enums with constructable!)
  • no generics supported yet (looks very possible)
  • limited number of params for the whole inheritance tree (default version compiles with 16, tested with 64)
  • only static structs/enums (no lifetimes)

Cost

I didn't perform any stress-tests. It should run pretty fast: there is no heap allocations, only some deref calls per construct! per defined prop per depth level. Cold compilation time grows with number of params limit (1.5 mins for 64), but the size of the binary doesn't changes.

Roadmap

  • add #![forbid(missing_docs)] to the root of each crate
  • docstring bypassing
  • generics
  • union params, so you can pas only one param from group. For example, Range could have min, max, abs and rel constructor params, and you can't pass abs and rel both.
  • nested construct inference (looks like possible):
#[derive(Construct, Default)]
pub struct Vec2 {
    x: f32,
    y: f32
}
#[derive(Construct)]
pub struct Div {
    position: Vec2,
    size: Vec2,
}
fn step_inference() {
    let div = construct!(Div {
        position: {{ x: 23., y: 20. }},
        size: {{ x: 23., y: 20. }}
    })
}

Contributing

I welcome contributions to Constructivism! If you'd like to contribute or have any questions, please feel free to open an issue or submit a pull request.

License

The constructivism is dual-licensed under either:

This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both.

You might also like...
Shows how to implement USB device on RP2040 in Rust, in a single file, with no hidden parts.

Rust RP2040 USB Device Example This is a worked example of implementing a USB device on the RP2040 microcontroller, in Rust. It is designed to be easy

Download a single file from a Git repository.

git-download Microservices architecture requires sharing service definition files like in protocol buffer, for clients to access the server. To share

A crate for converting an ASCII text string or file to a single unicode character

A crate for converting an ASCII text string or file to a single unicode character. Also provides a macro to embed encoded source code into a Rust source file. Can also do the same to Python code while still letting the code run as before by wrapping it in a decoder.

An mdBook single PDF generator using pure Rust and some Node.js

mdbook-compress An mdBook backend renderer to generate a single PDF file for a full book. There are other similar projects, but most rely on chrome in

Single-side boolean deserializers.

serde-bool Single value, true or false, boolean deserializers. Examples Supporting serde untagged enums where only one boolean value is valid, allowin

Simple time handling in Rust

time Documentation: latest release main branch book Minimum Rust version policy The time crate is guaranteed to compile with any release of rustc from

Fast and simple datetime, date, time and duration parsing for rust.

speedate Fast and simple datetime, date, time and duration parsing for rust. speedate is a lax† RFC 3339 date and time parser, in other words, it pars

Safe, comp time generated queries in rust

query_builder For each struct field following methods will be generated. All fields where_FIELDNAME_eq Numeric fields where_FIELDNAME_le where_FIELDNA

A real-time mixer

Pagana Pagana is a real-time mixer. This project is still in early stages of development and is not ready for any kind of production use or testing. D

Owner
polako.rs
Open tools & materials for game & app developers.
polako.rs
A single-producer single-consumer Rust queue with smart batching

Batching Queue A library that implements smart batching between a producer and a consumer. In other words, a single-producer single-consumer queue tha

Roland Kuhn 2 Dec 21, 2021
Compile time static maps for Rust

Rust-PHF Documentation Rust-PHF is a library to generate efficient lookup tables at compile time using perfect hash functions. It currently uses the C

null 1.3k Jan 1, 2023
Compile-time stuff and other goodies for rustaceans 🦀

?? bagel: Always baked, never fried bagel is a collection of macros and other things that we frequently use at Skytable, primarily to get work done at

Skytable 3 Jul 4, 2022
Create archives of files within Garry's Mod

gm_zip Create archives of files within Garry's Mod. Note: The scope of this module only works accross the gmod installation files e.g from GarrysMod/g

Earu 9 Oct 25, 2022
Data structures and algorithms for 3D geometric modeling.

geom3d Data structures and algorithms for 3D geometric modeling. Features: Bezier curve and surface B-Spline curve and surface Spin surface Sweep surf

Junfeng Liu 31 Sep 20, 2022
Library and proc macro to analyze memory usage of data structures in rust.

Allocative: memory profiler for Rust This crate implements a lightweight memory profiler which allows object traversal and memory size introspection.

Meta Experimental 19 Jan 6, 2023
Algebraic structures, higher-kinded types and other category theory bad ideas

Algar Algebric structures, higher-kinded types and other category theory bad ideas. Yes, you'll have generalized functors, applicatives, monads, trave

Stefano Candori 3 Jan 31, 2023
High-order Virtual Machine (HVM) is a pure functional compile target that is lazy, non-garbage-collected and massively parallel

High-order Virtual Machine (HVM) High-order Virtual Machine (HVM) is a pure functional compile target that is lazy, non-garbage-collected and massivel

null 5.5k Jan 2, 2023
An ownership model that is used to replace the Ring in Linux.

std-ownership An ownership model that is used to replace the Ring in Linux. It's 10x faster than Ring in Syscall. Overview The ownership system allows

Rhodes 4 Feb 13, 2023
A Platform-less, Runtime-less Actor Computing Model

CrossBus A Platform-Less, Runtime-Less Actor Computing Model Overview CrossBus is an implementation of Actor Computing Model, with the concept that Ru

Bruce Yuan 105 Apr 8, 2023