taurus-d / taurus

0 stars 2 forks source link

Enumify #10

Open iK4tsu opened 3 years ago

iK4tsu commented 3 years ago

Enumify - a tool to generate better Algebraic types

Field Value
Author João Lourenço (jlourenco5691@gmail.com)
Implementation https://github.com/taurus-d/taurus/pull/9

Abstract

This attempts to introduce one of the most important core features of Taurus lib. Enumify will be used to generate new Algebraic types in an easier way and allow for type repetition within the new type. It will have as dependency SumType, the new version of Algebraic, which is going to be added to Phobos in the next version 2.098.0.

Rationale

A more dynamic algebraic type

Right now the new SumType algebraic generator cannot take duplicated types. This is due to the fact that it cannot match two identical types. A workaround for this is to use type wrappers, such as a struct with an alias this.

struct TypeA { int i; alias i this; }
struct TypeB { int i; alias i this; }
alias MyType = SumType!(TypeA, TypeB);

This is an ok procedure, however, this is not the way if we want TypeA and TypeB to be part of MyType. Resorting to this solution makes those types independent, but there might be some case in which we want those types to be part of another type.

Some examples are Option from Scala and Rust, or Result from Rust. The Some type is an algebraic type defined by two variants, Some and None. These two variants are part of Option and not some independent types. Scala achieves this with classes, making Option abstract, and Rust can achieve this with their implementation of enum. Using classes is an option but not in this case. The use of classes makes GC usage a must and is not compatible with D_BetterC. The other alternative is to use SumType which is a robust algebraic type generator. However, to achieve this behavior, Option must become a sort of algebraic itself with lots of boilerplate code. Because we want to have the ability to have algebraic types with repeated types we must create wrappers inside the new type. Let's take an example at Result. How could we achieve this Result!(int, int)? Ok contains an int and Err as well.

struct Result(T, U) {
    public struct ok  { T handle; }
    public struct err { U handle; }

    public immutable SumType!(ok, err) handle;

    private this(ok  payload) { handle = payload; }
    private this(err payload) { handle = payload; }

    public static Ok ()(T t) { return Result!(T, U)( ok (t) ); }
    public static Err()(U u) { return Result!(T, U)( err(y) ); }
}

This is a solution to our problem. Not so bad, but what if we want an algebraic with lots of variants? Taking as an example, this enum from Rust:

enum Message {
    Move { x: i32, y: i32 },
    Echo(String),
    ChangeColor(u32, u32, u32),
    Quit,
}

These types can quickly become tedious to write, and let us be honest, all variants of algebraic type should be a part of the type itself and not an independent type.

Description

Enumify is a tool to be used inside a struct. The struct will be defined as the new algebraic type. Variants are defined using the @Member UDA that takes as the first argument a name, used to define the static function given as the example above, and the rest will define the types and names in a Tuple expression faction.

Usage

Option

import taurus.enumify;
import sumtype;

struct Option(T) {
    @Member!("Some", T)
    @Member!("None")
    mixin enumify;
}

void main() {
    Option!int option = Option!int(4);

    // https://issues.dlang.org/show_bug.cgi?id=21975
    assert(option.handle.match!(
        (Option!int.some) => true,
        _ => false
    ));

    option = Option!int.None;

    // https://issues.dlang.org/show_bug.cgi?id=21975
    assert(option.handle.match!(
        (Option!int.some) => false,
        _ => true
    ));
}

Result

import taurus.enumify;

struct Result(T, U) {
    @Member!("Ok", T)
    @Member!("Ok", U)
    mixin enumify;
}

Message

import taurus.enumify;
import sumtype;

struct Message {
    @Member!("Move", int, "x", int, "y"),
    @Member!("Echo", string),
    @Member!("ChangeColor", uint, uint, uint),
    @Member!("Quit"),
    mixin enumify;
}

void main() {
    with (Message) {
    Message message = Move(2, 3); // assign x then y
    message.match!(
        (move m) { assert(m.x == 2 && m.x == m[0]); }, // values stored as a `Tuple`
        _ { return; }
    );

    Echo("str").match!(
        (echo e) { assert(e == "str" && e == e.handle); }, // values stored as `alias this`
        _ { return; }
    );

    import std.algortihm : sum;
    import std.range : only;
    auto n = ChangeColor(250, 250, 250).match!(
        (_ cc) => sum(only(cc[0], cc[1], cc[2]))
    );
    assert(n == 750);
}}