Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement traits #527

Closed
Savio-Sou opened this issue Nov 28, 2022 · 4 comments
Closed

Implement traits #527

Savio-Sou opened this issue Nov 28, 2022 · 4 comments
Labels
enhancement New feature or request

Comments

@Savio-Sou
Copy link
Collaborator

Problem

Noir currently lacks abstract interfaces for types to implement.

Solution

Implement trait as in Rust.

Excerpt extracted from Rust by Example:

trait Animal {
    fn new(name: &'static str) -> Self;

    fn name(&self) -> &'static str;
    fn noise(&self) -> &'static str;

    fn talk(&self) {
        println!("{} says {}", self.name(), self.noise());
    }
}

impl Animal for Sheep {
    fn new(name: &'static str) -> Sheep {
        Sheep { name: name, naked: false }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        if self.is_naked() {
            "baaaaah?"
        } else {
            "baaaaah!"
        }
    }
    
    // Default trait methods can be overridden.
    fn talk(&self) {
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

Alternatives considered

Partially related issue on enabling abstract operations via implementing first-class functions: #467

@Savio-Sou Savio-Sou added the enhancement New feature or request label Nov 28, 2022
@jfecher
Copy link
Contributor

jfecher commented Nov 28, 2022

This is indeed related to first-class functions (#467) as that was our plan to emulate traits without implementing a complex trait solver and the complexity to the language that traits require. Your example could be written as the following once first class functions are implemented (though noir also doesn't have strings, I've mocked them here):

struct Animal<T> {
    new: fn(String) -> T,
    name: fn(T) -> String,
    noise: fn(T) -> String,
    talk: fn(T),
}

/// Return an animal instance with a default `talk` method
fn make_animal<T>(new: fn(str) -> T, name: fn(T) -> str, noise: fn(T) -> str) -> Animal<T> {
    Animal {
        new,
        name,
        noise,
        talk: |self| {
            println!("{} says {}", self.name(), self.noise());
        },
    }
}

global sheep_animal = Animal {
    new: |name| Sheep { name, naked: false },
    name: |self| self.name,
    noise: |self| if self.is_naked() {
        "baaaaah?"
    } else {
        "baaaaah!"
    },
    // "default" methods are normal fields that can be initialized normally, or with a helper like make_animal
    talk: |self| {
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
};  

This builds on concepts users already know (structs, struct initialization, globals), but may take some getting used to to use them in this new way. Since these are normal structs, functions using what would previously be trait bounds should take the struct as an extra parameter instead:

// Rust equivalent
fn name_and_noise<T: Animal>(animal: T) -> String {
    format!("{}{}", animal.name(), animal.noise())
}

// Noir equivalent (though String and format! do not exist in noir)
fn name_and_noise<T>(animal: T, instance: Animal<T>) -> String {
    format!("{}{}", instance.name(animal), instance.noise(animal))
}

The approach is more flexible than traits but requires users to manually pass them around. This is more explicit and possibly less confusing about what is going on under the hood, but is certainly more cumbersome to type. Using structs in this way means we can avoid the type-level programming of nudging the compiler to solve increasingly complex trait constraints and focus on the normal value-level programming that programmers are used to. Experienced trait/typeclass users will recognize this as the scrap your typeclasses approach.

Another nice aspect of this approach is that it does not preclude adding traits later and it faster to implement, allowing more features to be in developers hands in the meantime. In fact there is already a PR for adding them: #468.

@Savio-Sou
Copy link
Collaborator Author

Savio-Sou commented Nov 29, 2022

Another nice aspect of this approach is that it does not preclude adding traits later and it faster to implement, allowing more features to be in developers hands in the meantime.

Agree.

Created the issue more for keeping it tracked here on GitHub while we work towards Rust-resemblence in the mid-/long-term.

@SleepingShell
Copy link

SleepingShell commented May 18, 2023

Leaving a comment to show my interest in traits. For example, I have a struct in which it would be nice to use generics for different sized integers if the end-user chooses. However, this would require a lot of boilerplate and require passing math functions for every integer size just to compute e.g T + T. Or even casting to a field/integer.

@Savio-Sou
Copy link
Collaborator Author

Superseded by #2568.

@Savio-Sou Savio-Sou closed this as not planned Won't fix, can't repro, duplicate, stale Sep 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Archived in project
Development

No branches or pull requests

4 participants