problem-specifications icon indicating copy to clipboard operation
problem-specifications copied to clipboard

Exercise Idea: composite-resistors

Open sshine opened this issue 6 years ago • 11 comments

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:

  • The data type Resistor is given as a newtype wrapper around (Color, Color, Color).
  • But that means there is no constructor for a resistor with infinite resistance.
  • But that means some composite resistors don't have such a representation:
    let   r50 = Resistor (Green, Black, Black)
        r1000 = Resistor (Brown, Black, Orange)
        r1050 = getSum (Sum r50 <> Sum r1000)
    
    r1050 == Resistor (???)
    
  • But that's okay, because this representation has another bad property:
    let ten1 = Resistor (Brown, Black, Black)
        ten2 = Resistor (Black, Brown, Brown)
    
    ohms ten1 == ohms ten2 -- True
    ten1      == ten2      -- False
    
    (We don't compare Resistors for equality in -trio, so we live with it there.)
  • A necessary part of this exercise would then be to refactor the representation of -trio.
  • This is an excellent opportunity for property-based tests of monoidal laws.

sshine avatar Oct 14 '19 11:10 sshine

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:

  • The data type Resistor is given as a newtype wrapper around (Color, Color, Color).

    I think this isn't actually true; it's a typeclass instead.

  • But that means there is no constructor for a resistor with infinite resistance

    Wouldn't it be straightforward to define some zero-size-type implementing Resistor which always returned positive infinite resistance?

  • ...some composite resistors don't have such a representation

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

  • ...this representation has another bad property

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

coriolinus avatar Oct 14 '19 11:10 coriolinus

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.

coriolinus avatar Oct 14 '19 11:10 coriolinus

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.

yawpitch avatar Oct 14 '19 12:10 yawpitch

@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.

sshine avatar Oct 14 '19 12:10 sshine

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. ;-)

yawpitch avatar Oct 14 '19 12:10 yawpitch

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 avatar Oct 14 '19 12:10 sshine

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

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

sshine avatar Oct 14 '19 12:10 sshine

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.

coriolinus avatar Oct 14 '19 13:10 coriolinus

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...

yawpitch avatar Oct 14 '19 13:10 yawpitch

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

sshine avatar Dec 02 '19 23:12 sshine

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.

yawpitch avatar Dec 03 '19 13:12 yawpitch