Open erikerlandson opened 1 year ago
cc @armanbilge @cquiroz @benhutchison @jportway
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"
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.
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 :+1: 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!
@armanbilge my concerns with spire mostly stem from:
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.
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 <: (?, ?)]
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.
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 :)
@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.
It would really help me if spire
would supply some cleaner typeclass instances:
https://github.com/typelevel/spire/issues/1306
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]] ]
@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
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?
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 :grin:
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.
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:And furthermore the entire
ValueResolution
machinery: https://github.com/erikerlandson/coulomb/blob/scala3/core/src/main/scala/coulomb/ops/ops.scala#L180While 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.
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").scala.Conversion
orq.toValue
,q.toUnit
, etc.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 implementingAdd
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.