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

Simplifying coulomb operation signatures #534

Open
erikerlandson opened this issue Nov 15, 2023 · 15 comments
Open

Simplifying coulomb operation signatures #534

erikerlandson opened this issue Nov 15, 2023 · 15 comments

Comments

@erikerlandson
Copy link
Owner

When I designed the scala-3 port, I designed the operation signatures to support value semantics similar to traditional numeric value promotion: for example 1 + 2.0 => 3.0 (integer + double => double). I also pushed the implicit conversion of units into the same signatures, and so I have fairly complex implicit constucts like:

    transparent inline given ctx_add_2V2U[VL, UL, VR, UR](using
        nev: NotGiven[VR =:= VL],
        neu: NotGiven[UR =:= UL],
        vres: ValueResolution[VL, VR],
        icl: Conversion[Quantity[VL, UL], Quantity[vres.VO, UL]],
        icr: Conversion[Quantity[VR, UR], Quantity[vres.VO, UL]],
        alg: AdditiveSemigroup[vres.VO]
    ): Add[VL, UL, VR, UR] =
        new infra.AddNC((ql: Quantity[VL, UL], qr: Quantity[VR, UR]) =>
            alg.plus(icl(ql).value, icr(qr).value).withUnit[UL]
        )

And furthermore the entire ValueResolution machinery:
https://github.com/erikerlandson/coulomb/blob/scala3/core/src/main/scala/coulomb/ops/ops.scala#L180

While I was able to make all of this complexity work, and it was cool and fun, I have my doubts about whether it is a great design in the bigger picture.

  1. In general Scala data types do not do this kind of thing. Their operations assume type parameters that are the same.
  2. Many people considered the value promotions for numerics to be not a good idea, and would wonder why I'm trying to preserve them, at such huge expense
  3. Any implicit conversions are generally delegated to scala.Conversion and I can do the same. This will localize the conversion logic and simplify it substantially. If someone would prefer to not enable implicit conversions, they can just not import them (itself a simplification of "policies").
  4. My sense is that people tend to just work in the numeric (value) type that they want, and all of this complicated value promotion is mostly unused. If someone does need the conversion, it can either be accomplished via scala.Conversion or q.toValue, q.toUnit, etc.
  5. These complexities tend to metastasize. Every time I introduce new features, such as DeltaQuantity, or QuantityVector, this complexity of semantics propagates and multiplies the effort to introduce something new.

This would make coulomb-core smaller, simpler, and probably easier to understand. It also reduces compiler load, and probably makes the entire system a bit more stable. When implementing Add and friends, I often felt like the compiler barely supported what I was trying to do. Pulling back from that threshold a bit might be a good thing.

image

@erikerlandson
Copy link
Owner Author

@erikerlandson
Copy link
Owner Author

On the down side, doing this would itself be a somewhat substantial rewrite, including some of the documentation. But at least it would be a rewrite in the direction of "more simple and easy"

@erikerlandson
Copy link
Owner Author

I could, while I am at it, reconsider coulomb-spire as a separate project, and jettison my own internal implementation of Rational, as @armanbilge advocated originally. I still feel some reservations about how stable spire is, but at least it has a more solid footing into scala-3 now, which really jammed me up for a while. Again, doing this would enable me to reduce some code and complexity.

@armanbilge
Copy link
Contributor

I still feel some reservations about how stable spire is

What exactly are your reservations? Spire has been extremely stable for several years now.


Btw, overall big 👍 to the simplifications proposed here. Besides the UX improvements, it also lowers the bar to contributors and increases the maintainer bus factor. Thanks for the exposition here!

@erikerlandson
Copy link
Owner Author

@armanbilge my concerns with spire mostly stem from:

  1. the lengthy and painful transition to supporting scala-3, which blocked my own transition for something like a year.
  2. it doesn't appear to be under active development. Github shows most of the code not updated in multiple years, and the most recent commit was 6 months ago.

Regarding (1), I guess the transition is complete, so it's no longer an issue, unless we are some day presented with scala 4 :)

Regarding (2), maybe it's just an indication that the package is stable. There's only so much one can do, implementing numeric types.

Lastly, I really wanted to make coulomb-core small and minimalist. However, it didn't really turn out quite as small as I'd hoped any way, and with hindsight I now think it is regrettably hard to explain "oh, you need to import this policy, and by the way if you try to import both the native and spire policies it will break, etc". Not having to deal with that, and also not having to explain that to users, would be nice.

@erikerlandson
Copy link
Owner Author

erikerlandson commented Nov 15, 2023

As a side note, one of the things that has been interesting working with coulomb is noticing all of the subtle ways that Scala's type system is sort of "single-type-parameter centric" - coulomb's core data structure has two type parameters: Quantity[V, U], and it makes the mapping sometimes a bit subtle.

I occasionally wish I could make Quantity into a monad, but its two type parameters work against that.

I've been lately wondering if something like this would be feasible, and if so would it be worth it.

    // give Quantity a single type parameter, 
    // but force it to be of form `(V, U)`
    Quantity[VU <: (?, ?)]

@erikerlandson
Copy link
Owner Author

One of the most unusual facets of coulomb is that the unit parameter is a totally free type parameter - it is not associated with any actual concrete value, but only influences certain relations at compile time.

@armanbilge
Copy link
Contributor

the lengthy and painful transition to supporting scala-3, which blocked my own transition for something like a year.

Right, fortunately this was a one-time thing :)

To that point: have you encountered any significant issues using it on Scala 3? If not, I would say that transition was a success, and the Spire has stabilized its Scala 3 support (off the top of my head I can think of only one outstanding Scala 3-specific bug relating to the cfor macro).

it doesn't appear to be under active development. Github shows most of the code not updated in multiple years, and the most recent commit was 6 months ago.

Isn't that the definition of stability? 😜 if you are looking for a library that can move fast and break things to better support Coulomb's usecases, indeed Spire is not the right choice: at this stage we have little appetite for compatibility-breaking changes.

However, if there are specific things we can do to further stabilize Spire and make it more useful to Coulomb please let me know. Is it possible your reservations are about whether Spire is "maintained" rather than if it is "stable"? In that case, yes, I am maintaining it :)

@erikerlandson
Copy link
Owner Author

@armanbilge no, I have had no issues with spire. As you say, inactivity is often a warning sign that projects are no longer maintained. I wondered if you were maintaining it, I did notice you were the most recent commit :)

I think the only issue I had with spire, was constructing a matrix of conversions. Not all of its types convert directly to each other, and I'm pretty sure I get why, since it involves making some choices in terms of precision in some cases. Getting rid of value promotion may reduce the need for that, but either way I have a solution.

I'm leaning toward just deciding to make it a coulomb-core dependency.

@erikerlandson
Copy link
Owner Author

It would really help me if spire would supply some cleaner typeclass instances:
typelevel/spire#1306

@benhutchison
Copy link

Hi Erik, apologies for the slow response on this.

We've discussed the Spire dependency in another thread, but as to the primary topic of this proposal, simplifying the signatures and eliminating cross-value operations, I'm good with that.

From time to time I do combine Ints and BigDecimals (typically when treating an Int as a way to make a Decimal), or add an Int to a Long timestamp. But these will be readily handled with explicit conversions.

Intrigued but unsure about idea of expressing Quantity with a single type Quantity[(V, U)]. Are there specific things it would unblock?

Seems like we can derive single-type-param TCs (eg Monoid[Quantity[U, V]]) for Quantity already. This is the possibly dirty way I've been doing it.

given (using mv: Monoid[V]): Monoid[Quantity[V, U]] = mv.asInstanceOf[Monoid[Quantity[V, U]] ]

@erikerlandson
Copy link
Owner Author

@benhutchison I'm not really sure if a single (V, U) type is ultimately a good idea, but it might make it easier to work with typeclass systems such as Monad[T] or others, which tend to assume a single type parameter.

It's clean to define additive monoid for Quantity[V, U] since U doesn't change. multiplicative monoids don't work because it's not closed with respect to U. Interestingly, RuntimeQuantity makes this easy since the unit is always just RuntimeUnit

@benhutchison
Copy link

It's clean to define additive monoid for Quantity[V, U] since U doesn't change. multiplicative monoids don't work because it's not closed with respect to U.

Great point, thanks for the reminder. 🤔 💭 actually I wonder... is that distinction among Monoids visible in algebra/math?

@erikerlandson
Copy link
Owner Author

is that distinction among Monoids visible in algebra/math?

That is a very interesting question - I gave a talk one time on my unit-aware matrix algebra, and a mathematician in the audience said "we don't generally care because the matrixes are all isometric" - and although I care, his point was solid 😁

I think even if you are tracking the units, they are probably still multiplicative monoids if you take the view that the "objects" are "algebraic terms with number and unit factors in them" - so I suppose that would make them more or less like RuntimeQuantity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants