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

Why Quack? #10

Closed
bvssvni opened this issue Jan 24, 2015 · 0 comments
Closed

Why Quack? #10

bvssvni opened this issue Jan 24, 2015 · 0 comments

Comments

@bvssvni
Copy link
Member

bvssvni commented Jan 24, 2015

Assume we have the following trait with a property pattern:

pub trait Foo {
    set_x(&mut self, val: f64);
    get_x(&mut self) -> f64;
    set_y(&mut self, val: f64);
    get_y(&mut self) -> f64;
    set_z(&mut self, val: f64);
    get_z(&mut self) -> f64;
}

What if we had another trait Bar that only needed x and y? Should we make Foo inherit Bar to save code? Or, perhaps we should just use Foo even if we don't need z? The larger libraries you write, such problems tends to get hairier and uglier to deal with.

This problem can be solved with something called "duck typing". Instead of creating large traits with many methods, you try to make them as small as possible.

pub trait X {
     set_x(&mut self, val: f64);
     get_x(&self) -> f64;
}

Now, you can make Foo inherit X:

pub trait Foo: X + Y + Z {}

There is a problem with this: What if we can set an x value, but not get one? We could split the traits into GetX, SetX etc.

Yet, there is another problem: Some people use a get_mut/get pattern. get_mut only works when there is an interior pointer to the type. For example, it won't work if you need to convert an i32 to i64.

Then, there is another problem: Some people like the builder pattern:

let x = Foo::new(...).bar(1.0).baz(2.0);

Then, there is yet another problem: In generic code, people might want to put a shared object inside a struct with &RefCell<T> or Rc<RefCell<T>>, but they also might want it work with an owned object:

struct Foo<T> {
    a: T,
}

struct Foo<'a, T> {
    a: &'a RefCell<T>
}

struct Foo<T> {
    a: Rc<RefCell<T>>
}

This can lead to inconsistent API design where one library might not work with another because it uses different smart pointers. Sometimes we want more control in performance sensitive code to pick a smart pointer implementation, but when this is not case, we just want to have a nice get/set pattern.

Let's write up all the problems so far:

  • Shared properties between traits
  • Incompatible get/set patterns
  • Builder pattern
  • Smart pointers in generic code

When using Piston-Quack, you define a get/set pattern by using GetFrom and SetAt.

impl<'a> GetFrom for (Position, Label<'a>) {
    #[inline(always)]
    fn get_from(label: &Label<'a>) -> Position {
        Position(label.pos)
    }
}

impl<'a> SetAt for (Position, Label<'a>) {
    #[inline(always)]
    fn set_at(Position(pos): Position, label: &mut Label<'a>) {
        label.pos = pos;
    }
}

This can be shortend down by using the quack! macro:

quack! {
    label: Label['a]
    get:
        fn () -> Position [] { Position(label.pos) }
    set:
        fn (val: Position) [] { label.pos = val.0 }
    action:
}

When using the library, we can import the Get and Set traits:

use quack::Set;
use Position;

foo.set(Position([1.0, 2.0]));

This also has the advantage that the imported type Position is the same as how you call the method.

More, it simplifies process when designing a new API:

  1. Add structs and enums for the data types
  2. Add GetFrom/SetAt/ActOn impls
  3. Done!

Now, if we want to extend the API with new methods, we could make the traits inherit the properties!

Here is an example:

pub trait RelativeTransform: Clone 
    where
        (Transform, Self): Pair<Data = Transform, Object = Self> + GetFrom + SetAt
{
    /// Appends transform to the current one.
    #[inline(always)]
    fn append_transform(&self, transform: Matrix2d) -> Self {
        let Transform(mat) = self.get();
        self.clone().set(Transform(multiply(mat, transform)))
    }

    // a bunch of other methods
    ...
}

The Pair constraint is necessary at the moment to make Rust understand the tuple. This might disappear when Rust gets full equality constraints.

Now we can auto implement the trait for all types that has these properties:

impl<T: Clone> RelativeTransform for T
    where
        (Transform, Self): Pair<Data = Transform, Object = Self> + GetFrom + SetAt
{}
@bvssvni bvssvni changed the title What duck typing is and what problems it solves Why Quack? Jan 28, 2015
@bvssvni bvssvni added draft and removed discussion labels Jan 28, 2015
@bvssvni bvssvni closed this as completed Feb 2, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant