-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Implements flatmap #44792
Implements flatmap #44792
Conversation
This PR defines Less importantly, I would also argue the same with |
That really goes to the heart of the matter, @jakobnissen , thanks. In functional programming languages such as Haskell and Scala, the optional type behaves very much like a container that can contain at most one element. It can be iterated, mapped, for-eached, etc. In Julia, the official alternative for the optional type is the union of If the designers of Julia come to understand, however, that Some and Nothing must not behave like a proper option monad, perhaps because this was the case for a long time and introducing this now feels like and afterthought or whatever, then maybe we should consider the question of how could we implement a proper optional type. About having non-containers being iterable, I would point out that right now integers, for example, are actually iterable in Julia
I would actually recommend against this behavior, given that integers are not containers in principle. I believe Of course I would actually also argue that the use of Regarding the complete patch, I would also like to point out |
It's correct that integers and chars are iterable in Julia. I find that objectionable, both from a conceptual point of view (integers are not containers), and from a practical (I often bump into bugs that would not be possible if not for this behaviour). I suppose one could define Less abstractly, adding
, and these different ideas about Implementing a new type that behaves like |
@jakobnissen I very much understand the desire to be conservative about introducing I would still like to invite you to step back for a bit, and try to look at this proposal from a perspective of following the nice programming-language theory and wisdom coming from functional programming, Haskell and Scala being merely two examples I'm most familiar with, but we can go all the way back to John Backus' Nobel lecture, or any good Category Theory book. I'm inviting you all to ask the question "what is Nothing anyways?". Or maybe, following Julia tradition, we might call this PR "Taking Nothing seriously"! Thanks for enumerating those two related issues. You seem to have a genuine interest in the issue and I hope I can make the case for the importance and benefits of pursuing the FP-paradigm optional, even on these stdlib types. You listed three positions. Regarding the first position, I would actually argue that this is completely in line with my proposal. In fact, Regarding the second point, of Pardon if I disagree, but the conclusion must be that these positions are not conflicting at all. Special-value use is consistent with You are very right to point out that the use of
I would argue that every use of Of course that's not up for me to decide. But the fact To conclude, I would like to make the point that this topic is even larger than that. There are many such types that "contain" a value, and then we have special operations to construct the value, and to fetch the value... That's what the monad stuff is all about. And Apologies for the long screed, I'm not sure this is the proper forum to discuss a proposal like that. I would also like to stress that this is coming from me scratching my own itch. I frequently code using "do-syntax" with map, because it's higher-level than for, and beats comprehensions because it's multi-line, but then you hit this limitation, and using explicit I would love to hear more thoughts about this, especially from Mr. Bezanson! I'm not very familiar with LISP and I'm not sure how a LISP programmer sees this. Is |
Ref #16961? |
Let me try to explain why I think A programming language is just as much defined by what bugs it doesn't allow you to write, as it is by what it does allow you to do. This sounds like an empty truism, but it really is true - I spend about as much time debugging and verifying correct behaviour of code as I do writing it in the first place. Hence, considerations about preventing footguns should have at least as much weight as considerations about new functionality, and arguably more than considerations about convenience. So, why am I so certain that adding new methods such as Consider a widely regarded bad PL design: The billion dollar mistake, the null pointer. What is so bad about the null pointer? Isn't it functionally equivalent to I think the aspect that made the null pointer one of the worst design decisions in PL history is the fact that it presents an edge case that is difficult to reliably guard against. Any given instance specified to be of type If struct Gene
DNA::Union{String, Nothing}
end
count_ts(x::Gene) = count(isequal('T'), x.DNA)
genes = [Gene("TATCGAGTA"), Gene(""), Gene(nothing)]
map(count_ts, genes) If Hence, in this regard, if methods are implemented for And that's why I think adding methods to |
@jakobnissen Your example demonstrates why using In your example I would suggest the issue is how a low level type, In Julia It is precisely to enable this kind of safe and strict programming to handle optionals, that it would be good to be able to I cannot say what was intended in Julia with Maybe this could be something for Julia 2.0? Or should we implement a separate iterable |
This package illustrates what I'm looking for, using |
I'm strongly against the To explain this, let us revisit the functor law:
Does this hold in Julia? jjulia> isequal(Iterators.map(identity, 1:2), 1:2)
false Unfortunately not. However, I argue that ==′(a, b) = isequal(collect(a), collect(a)) (let me allow ignoring details like how to handle more difficult cases like This is what I mean by " When designing functors, it is important to understand what equivalence class we are working on. This is in the end same as what @jakobnissen said initially. People like to think that I also argue this is not bad and also not unique to Julia (cf "setoid hell"). I think it's good to be inspired by other languages to make Julia better. But, since Julia is a very unique language, I think we need to carefully distill the idea. (FWIW, I'm also curious about making "nice" Maybe/Optional and Either/Result APIs in Julia. Here's my shot at giving a simple unified interface https://github.com/tkf/Try.jl#focus-on-actions-not-the-types) |
Thank you, @tkf . Would you mind making a concrete proposal for how we might have a flattable optional type in the standard library? All I want is to be able to write something like |
If you want to write the function body, using tuples as you suggested sounds good to me: Iterators.flatmap(randn(111)) do x x>0 ? (x^2,) : () end If you already have an Option-valued function and want to feed it to f(x) = x>0 ? Some(x^2) : nothing # this is given
Iterators.flatmap(Try.astuple ∘ f, randn(111)) |
Thank you @tkf, I agree. Long story short, triage yesterday was ok with In fact this can be considered a deficiency of the |
Thanks, @JeffBezanson . It sounds like It would still be nice to have an iterable With or without these neat types, having flatmap and also an |
Integers being iterable really is just an accident. We attempted to remove it for Julia 1.0, but by the time we tried to remove it, doing so would have broken an annoyingly large amount of code, so we didn't. It's a major source of bugs (I write some variation of |
I've put an |
I have changed the function name from
|
Just to be clear, I didn't mention |
Understood, I'll split it in two PRs |
flatmap is the composition of map and flatten. It is important for functional programming patterns. Some tasks that can be easily attained with list-comprehensions, including the composition of filter and mapping, or flattening a list of computed lists, can only be attained with do-syntax style if a flatmap functor is available. (Or appending a `|> flatten`, etc.) Filtering can be implemented by outputing empty lists or singleton lists for the values to be removed or kept. A more proper approach would be the optional monad, though, usually implemented in Julia as a union of Some and Nothing. This patch therefore also implements iteration methods for Some and Nothing, to enable the filtermap pattern with flatmap.
@tkf Would you like to write, or for me to write a bit more in the docstring, or just leave it like that? |
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! I think it's good to go.
Thanks a lot @tkf |
Ah, thanks for catching these points! @nlw0 Can you address @fredrikekre's points? (Remove the comment, add news and compat) |
Sure thing, hopefuly later today |
Little bit late to the game but: how is I.e. this seems to just add another short verb for a nearly trivial composition of two more basic verbs? Bit grouchy, I realize, but from reading the discussion it was not clear to me why it is preferable to introduce a new function for this essentially trivial composition (unless there's e.g. a performance argument)? |
Hi @thchr , the need for flattening maps arise often in more complex tasks. Having
One particular example when this is very useful is if you have a |
@thchr It's a great question if there may be performance considerations. I think there may be cases where a custom |
But composition already allows you to write this equivalently as: (flatten ∘ map)(1:3) do j
(flatten ∘ map)(1:3) do k
j!=k ? ((j,k),) : ()
end
end On the face of it, you save 8 characters for every
I think this is my main counterpoint to adding functions like this: it increases the vocabulary of the language and reduces the chance that everyone can read a bit code without needing to look up functions in the "dictionary"/docs. |
I do believe there might be performance benefits, I was motivated at first only by high-level reasons. I'll gladly look into that if I find the time. I am used to the function being available in other languages. In my opinion, it's worth having it. I have pointed out the importance to different applications. It's not just about It's not about coding golf. Some people are probably more familiar with the importance of In fact, I suspect there may be classes in the language today where the API implements a key function with a custom name, but it might actually have been just another I'm not really sure what else I can write, and it's a pretty long PR discussion already. I wish I had more compelling theoretical and computational arguments I could just lay down. In the end I'm mostly inspired from experience, and made the patch to see what the community thinks. I very glad I have found some acceptance. I hope others can help address these concerns. Best I can do is to try looking for examples of efficiency gains, I bet they do exist. |
To address @thchr's complaint: Why can't this just be a method We have (This isn't in 1.8, BTW, so no great hurry.) |
@mcabbott I could live with flatmap functionality being attained through an optional function argument provided to |
Array I find the array-based Regardging eager vs lazy
|
Related to #44294
flatmap is the composition of map and flatten. It is important for functional programming patterns.
Some tasks that can be easily attained with list-comprehensions, including the filter-mapping ([f(x) for x in xx if p(x)]), or flattening a list of computed lists, can only be attained with do-syntax style if a flatmap functor is available. (Or appending a
|> flatten
, etc.)Filtering can be implemented by outputting empty lists or singleton lists for the values to be removed or kept. A more proper approach would be the optional monad, though, usually implemented in Julia as a union of Some and Nothing.
This patch therefore also implements iteration methods for Some and Nothing, to enable the filter-map pattern with flatmap.
Highlights:
flatmap
over a collection of optionals is not equivalent to a composition of map and filter.map
etc, is necessary to make Some-Nothing equivalent to optional monads in other languages, from Haskell and Scala to C++17. Right now amap(sqrt, Some(4))
wouldn't work in Julia, though. Is this something we would like to pursue?