exercism / problem-specifications

Shared metadata for exercism exercises.
MIT License
326 stars 541 forks source link

Exercise Idea: composite-resistors #1608

Open sshine opened 4 years ago

sshine commented 4 years ago

This exercise is proposed in the context of the Haskell track, but I'm posting it here because I like the idea of sharing exercise ideas across, and monoid abstractions exist in other languages, too. In the process of porting resistor-color-trio in exercism/haskell#869, it occurred to me that resistors are monoids in two ways:

When you put them in series, they form an additive monoid (resistance adds up) with (black, black, black), better known as a wire, as the identity resistor. And when you put them in parallel, you don't exactly get a multiplicative monoid, since n resistors in parallel add up like 1/R = 1/R₁ + 1/R₂ + ... + 1/Rₙ. What's the identity resistor for a parallel circuit?

It isn't a wire, since then the current would follow the path of least resistance, which would always end up being the wire. A neutral resistor in a parallel circuit is any non-conductive material with infinite resistance. For example, my willingness to code in another language than Haskell.

Putting resistors in series and in parallel is neat in practice for making non-standard resistors or when you run out of a certain kind of resistor.

Haskell has some machinery for dealing with types that are monoids in more than one way: while one can define instance Monoid (Sum Resistor) where ... for the built-in Sum type, it seems necessary to define one's own Parallel type and make instance Monoid (Parallel Resistor) where ... instead.

Having this exercise in succession of resistor-color-trio leads to interesting thoughts:

coriolinus commented 4 years ago

I haven't used Haskell at all for half a decade, and I was only at a student level even then, so I'm going to attempt to recast this in Rust terms that I'm more familiar with. I think you're saying that the key fact about a resistor is that it has an Ohms value. Whether it's a single component, or a combination of other resistors, is immaterial.

In Rust, we'd express that like this:

pub trait Resistor {
    fn ohms(&self) -> f64;
}

We would then have students re-work their previous resistor implementation in terms of this trait:

impl Resistor for (Color, Color, Color) {
    fn ohms(&self) -> f64 {
        // students adapt existing work to fill this in 
        unimplemented!()
    }
}

We'd then have students write the composite types Serial and Parallel, which also implement Resistor and compute the appropriate resistances:

pub struct Serial {
    rs: Vec<Box<dyn Resistor>>,
}

impl Resistor for Serial {
    fn ohms(&self) -> f64 {
        unimplemented!()
    }
}

pub struct Parallel {
    rs: Vec<Box<dyn Resistor>>,
}

impl Resistor for Parallel {
    fn ohms(&self) -> f64 {
        // students fill this in with existing work
        unimplemented!()
    }
}

If I have understood you correctly, then I agree: this sounds like an interesting continuation of the resistor exercises.


WRT your additional thoughts:

coriolinus commented 4 years ago

Also, this may be dogshedding, but I'd suggest the title composite-resistors instead of resistor-monoid; even if these are in fact monoids, calling them that sounds weird in every language I know which isn't Haskell.

yawpitch commented 4 years ago

A neutral resistor in a parallel circuit is any non-conductive material with infinite resistance. For example, my willingness to code in another language than Haskell.

Funny, that ... thus far Haskell is the only serious language I've encountered that seems to pride itself on being anti-conductive and infinitely resistant.

sshine commented 4 years ago

@yawpitch: I wasn't talking about Haskell, I was talking about my willingness. ;-)

Aren't anti-conductive and infinitely resistant the same? As for what this means metaphorically for a programming language, the learning curve is pretty steep, but the language is highly conductive to abstract thought. Haskell has excellent FFI, too.

Whenever I advocate for Haskell in the real world, the real world seems to be the resistant part.

yawpitch commented 4 years ago

As for what this means metaphorically for a programming language, the learning curve is pretty steep, but the language is highly conductive to abstract thought. Haskell has excellent FFI, too.

Don't get me wrong, I'm fascinated by Haskell. But then I'm also fascinated by Dwarf Fortress... until recently I simply hadn't understood that I'm a Masochist, which is also a Monoid.

Whenever I advocate for Haskell in the real world, the real world seems to be the resistant part.

... there might be a reason for that. ;-)

sshine commented 4 years ago

I don't see what's wrong with saying that nonequal resistors can have equal resistance

I suppose you're right.

It depends on whether or not you want to model structural equality of composition.

I thought it would be simpler to have equality for resistors mean only that they have the same resistance. This can be done with either a custom Eq instance rather than a structurally derived one, or better yet, another representation.

(Again, there's a lot of things about real resistors that are ignored, such as its uncertainty -- since I'm neck-deep in these exercises, I started thinking about another exercise that also models uncertainty.)

That's because Resistor shouldn't be a newtype wrapper, but a typeclass/trait.

pub trait Resistor {
    fn ohms(&self) -> f64;
}

OK, so an implication of that is that your representation of Resistor becomes a function, and composition of resistors becomes function composition. This means that large compositions will cause thunking, but this is good, since strictness is opt-in.

It made me think of something my professor once said (in the context of tail-recursion on trees in Standard ML): "you probably want to reify your continuations and use a more concrete data type." I don't know if that applies here, though.

sshine commented 4 years ago

I'm a Masochist, which is also a Monoid.

I collect things that are monoids; how are masochists monoidal?

coriolinus commented 4 years ago

OK, so an implication of that is that your representation of Resistor becomes a function, and composition of resistors becomes function composition. This means that large compositions will cause thunking, but this is good, since strictness is opt-in.

Yes, that's true, but assuming resistors are immutable (and I can't think of a reason to mutate a resistor), it seems to be straightforward for composite resistors to cache their value eagerly. Given arbitrarily-deep resistor trees, doing so might potentially be a useful optimization.

yawpitch commented 4 years ago

I'm a Masochist, which is also a Monoid.

I collect things that are monoids; how are masochists monoidal?

Admittedly I'm still learning Haskell, but a Masochist is a NewType of Human, and they're monoidal over addition (bring two Masochists together and you get a bigger Masochist) and multiplication (the Masochism of a group of Masochists is greater than the sum of its parts), and probably exponentiation...

sshine commented 4 years ago

Quick note: In Haskell, one should probably use a GADT or an existentially quantified type rather than using a type class.

yawpitch commented 4 years ago

Quick note: In Haskell, one should probably use a GADT or an existentially quantified type rather than using a type class.

Might be good to see an example of that, for the rest of us.