-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Redesign NonEmptyLazyList
to be maximally lazy
#4504
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
// allows the creation of fully lazy `NonEmptyLazyList`s by prepending to this | ||
def maybe[A](ll: => LazyList[A]): Maybe[A] = new Maybe(() => ll) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, this method requires a way more comprehensive scaladocs (preferably with examples).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haven't written the docs yet, lol; not gonna do that until the design is solid enough to move forward, so I don't waste effort (docs are hard).
that said, the explanation is fairly simple: fromLazyList
and fromLazyListUnsafe
are bad, because they evaluate the first element of the LazyList
. to remedy that, Maybe
allows you to prepend either single elements or NonEmptyLazyList
s, guaranteeing that the result is not empty without evaluating any elements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trivial usage example:
val nell: NonEmptyLazyList[Int[] = 1 #:: NonEmptyLazyList.maybe(LazyList.empty)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NthPortal to be honest, I'm still not getting what the purpose those Maybe
and Deferred
classes are supposed to serve for. IMO by design LazyList
is a collection that lazily evaluates its items upon the first traversal. But the instance of LazyList
is not supposed to be lazy itself. Therefore it would be nice to have examples that would show how exactly those classes can be useful. And why the same functionality cannot be achieved without them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see that Deferrer
is a similar thing to one in the std LazyList
, so it seems to be fine then. But Deferrer
is just a syntax that is added implicitly and hence transparently for users. Which is not the case for Maybe
though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@satorg I'll look into adding an example or two to the docs of maybe
Make `NonEmptyLazyList` properly lazy and remove unnecessary laziness.
This commit does not include docs or tests.
DummyImplicit
no more private var
9c4a5ec
to
e948a1d
Compare
better fromLazyListUnsafe
/** Prepends a `LazyList`, yielding another [[Maybe]]. */ | ||
def #:::[AA >: A](prefix: => LazyList[AA]): Maybe[AA] = | ||
new Maybe(() => prefix #::: ll) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt this method belongs here.
Prepending one LazyList
to another one lazily is the job that LazyList.Deferrer
seems to be doing already. I.e.:
def a: LazyList[Int] = ???
def b: LazyList[Int] = ???
val maybe1 = NonEmptyList.maybe(a #::: b)
// the above seems to be the same as
val maybe2 = a #::: NonEmptyList.maybe(b)
Anyway a method that only involves LazyList
on both sides does not seem to be a part of the NonEmptyLazyList
DSL to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this question is... tough. conceivably, one might build up a result something like as follows:
def buildResult[A](maybe: NonEmptyLazyList.Maybe[A]): NonEmptyLazyList[A] =
if (something) someNell #::: maybe
else if (somethingElse) a #:: b #:: maybe
else buildResult(someLL #::: maybe)
}
buildResult(NonEmptyLazyList.maybe(LazyList.empty))
now, you could argue that you still don't need it, and can construct the Maybe
instance later. which is true. but that seems uglier and less intuitive to me. would love to get thoughts from more people.
Edit: this is example is awful, for reasons that I will explain in another comment below. I'm honestly ashamed to have written it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could argue that you still don't need it
Yeah, indeed, I would :)
Again just IMO: the entire Maybe
does not seem that it should be a part of NonEmptyLazyList
per se. Because it looks like a bunch of extension methods for LazyList
that simply helps to construct a new NonEmptyLazyList
out of it.
For example, see cats.syntax.ListSyntax
and its corresponding ListOps.toNel
, etc.
So perhaps it would make sense to consider creating a cats.syntax.LazyListSyntax
which would provide all this functionality as a bunch of extension methods only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the issue is, NonEmptyLazyList.fromLazyList
evaluates the first state/element of the LazyList
, which is Bad™; NonEmptyList.fromList
does not have this concern as the List
is already fully evaluated. the intent behind Maybe
was to have a way to construct a NonEmptyLazyList
without evaluating any elements. now that fromLazyListUnsafe
has been changed to be lazy, Maybe
isn't needed as much, though I maintain that it is still a useful tool. its design could perhaps use a bit more workshopping though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been sick for a week, so I haven't been able to address things, but I'm hopefully better enough to get back to this.
the reason we need some sort of wrapper around LazyList
with a #:::
operator is that, despite it syntactically being a right-associative prepend operator, #:::
actually behaves semantically as a left-associative append operator.
this also means that my example earlier was terrible, and the design of Maybe
isn't ideal either. I really have been off my game recently.
I'll re-evaluate and redesign this part of the API to be a bit more reasonable with this in mind
/** Prepends a `NonEmptyLazyList`. */ | ||
def #:::[AA >: A](prefix: => NonEmptyLazyList[AA])(implicit d: DummyImplicit): NonEmptyLazyList[AA] = | ||
create(prefix.toLazyList #::: nell().toLazyList) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need this overload with NonEmptyLazyList
type if we already have another one that takes LazyList
? I am a bit concerned about this overload trick. Usually, overloaded method names are discouraged unless absolutely necessary. But it does not seem to be the case here, because prepending a NonEmptyLazyList
can be easily achieved with just the single method above that takes LazyList
only:
def a: NonEmptyLasyList[X] = ???
def b: NonEmptyLasyList[X] = ???
val c: NonEmptyLazyList[X] = a.toLazyList #::: b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be weird to have NonEmptyLazyList
be a second-class citizen of its own API. however, the meaning of the methods are identical, yielding no additional conceptual weight and making the API easier to use. additionally, it's not possible to accidentally use the wrong method either, as they are only the same after erasure. I don't personally see any issue with it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it pretty much corresponds to the way how other APIs are organized in Cats. See for example NonEmptyList.concat
vs NonEmptyList.concatNel
, also NonEmptyVector.concat
vs concatNev
, etc. Retrospectively, it would not be a problem to overload concat
everywhere in the way as you're proposing. But for some reason the decision was to avoid overloading and provide clearer separate names wherever possible. I personally support it because feel a bit sour about having any sort of "hacks and tricks" in code – they are usually easy to add but tend to turn into maintenance hell in long-term run.
That said, NonEmptyList
has the :::
which takes another NonEmptyList
, not just List
. Perhaps it is just a legacy of that particular class because it was the very first NonEmpty*
one. Anyway, it is still not overloaded.
laziness fixes, oops
NonEmptyLazyList
properly lazyNonEmptyLazyList
to be maximally lazy
improve API, renaming Maybe to Tail
…alled examples and fix mistake
First of all, I have to say that I really appreciate this effort to make NonEmptyLazyList better, thank you @NthPortal for taking on that! However, let me add my 2 cents worth and outline a few thoughts regarding the APIs being developed. The thoughts may or may not be that good – just my personal perception of the issue:
|
it's rather moot given the above, but regardless, I strongly disagree. despite being symbolic methods,
this is possible, but seems unnecessarily confusing and annoying to users. the methods do the same thing, so they should be named the same thing |
I really like this idea, though I think something like |
Redesign
NonEmptyLazyList
to be maximally lazy.