Closed kazmasaurus closed 6 years ago
FWIW, I'm a fan of setup blocks, but I'd rather keep the operator as simple as possible and use some helper function to do mutation, e.g.:
func mutate<T>(f: T -> ())(x: T) -> T {
f(x)
return x
}
class Foo {
var bar: Int = 0
}
let foo = Foo() |> mutate {
$0.bar = 1
}
println(foo.bar)
@kazmasaurus First off, nice work! I totally get where you’re coming from, and I dig the approach.
I’m similarly unsure about the operator and whether Prelude is the right home (although I can’t think of a better one off the top of my head); I’m going to have to give that some more thought.
@narfdotpl’s point re: a mutate
function is particularly interesting. I’ve been working with Lens
es a lot lately, and there’s a semi-standard Lens
method named modify
which is much like the suggested mutate
. I’m not 100% sure, but if modify
were a free function instead of a method, an overload for arbitrary setter-style functions might make a lot of sense, and I suspect it’d look a lot like mutate
.
I’ve been thinking about shipping Lens
as a µframework; that might make a better home. In the meantime, I’ll keep thinking about it, but please don’t hesitate to share your thoughts!
I've been having quite a bit of fun screwing around with this off and on over the past couple days, and I've definitely accumulated some thoughts. Let the thought sharing commence.
mutate
is awesome.I think @narfdotpl is absolutely right that doing this through a function is a better solution. It both doesn't add confusion to the operator and helps clarify what's going on.
I wanted to see how much cruft a more Swiftlike method signature (with no currying and a trailing f
) would add to the process. The big advantage being that it would then suck in the exact same way as all of the standard library's higher order globals, which would avoid confusion about which functions take parameters Swiftly and which take them Haskelly. The results aren't terrible:
let foo = Foo() |> (flip(mutate) <| {
$0.bar = 1
})
// Works, but the multi-line closure in the parens makes me a little 😞.
let foo = Foo() |> curry(mutate) <| {
$0.bar = 1
}
let foo = Foo() |> curry(flip(mutate)) {
$0.bar = 1
}
// Maybe a bit much, but gives us a nice Swifty trailing closure.
This last one of course led me to try to package that up in an operator which belongs in a different PR (or purgatory, not sure which).
let foo = Foo() |> mutate~ {
$0.bar = 1
}
This blog post that recently came across the twitters is attempting to implement what we have here, but calls it tap
because of some prior art (and takes an A -> A
closure, which seems weird to me since you then end up with a type constrained map
instead of ruby's tap
). tap
as a name is interesting, but is optimized for the logging case (particularly in chains, which is a use I hadn't thought of) and just happens to generalize to setup blocks instead of the other way around. I'm not sure the name makes sense outside the context of a chain though, nor does it make sense---if we're going to abuse the beer analogy---to tap a keg to add/change/mix ingredients.
The only thing that comes to mind that could cover both uses is affect
(or hereBeSideEffect
, but that'll have to wait until Sept.). But then have we gone from names that makes sense in only one of the two contexts to one that makes sense in neither?
The other possibility is that tap
is a more fitting name for the setup block case than I'm giving it credit for. Having a name that matches prior art is compelling too, especially since it appears in other libraries under that name as well.
The Lens
stuff is definitely interesting, and something I probably need to actually read more about (I sort of get the idea, but am nowhere near actually grokking it yet). I also wouldn't be disappointed if this ended up in a Lens
µframework. That said, one reason not to move it into one is approachability. One of the things I like about the idea of a setup block is that it's just a nudge towards getting people to think about what something is instead of how to create it, and putting it in a Lens
µframework might make it seem a lot less approachable.
But if not in Lens
, then where? Something that could be nice to have is a µframework that just fleshes out the standard library's free functions without adding types or tons of operators (says the guy about to open a PR for a new operator). I personally have been sort of thinking about Prelude
as that µframework, but I don't think that's what you actually intended. It's just that right now Prelude
accidentally happens to be the closest thing.
The only other thing that comes close would be Dollar.swift, but between the weird namespacing, the name scheme conflicts with---or outright rewrites of---stuff that already exists in the standard library, and the bloat, Dollar strikes me as trying to create an entirely new API instead of just augmenting the current one.
I've also thought about a new µframework that sort of tries to pair with Prelude
to fit the bill, but I can't come up with a clear way of describing what would belong to Prelude
and what would belong to the sibling, which make me think that they're not actually different things. (The best I've got would be breaking apart Prelude
into one µframework for basic functional concepts (application, composition, curry
, flip
, and the flurry
operator mentioned above), and one with common helper functions (id
, const
, etc.), but I'm still not sure that this is an actual distinction, nor can I decide where fix
would go)
I think of Prelude’s purpose as combinators & other helpers for functional programming in Swift. Splitting id
away from curry
doesn’t make much sense from that angle; they both belong.
I also think: does it introduce new types? Then it probably belongs elsewhere. If not, does it exist in Haskell’s prelude? Then it probably belongs here.
Inspired by this pull request, I've been using mutate
a lot in my game lately. I really like it:
func mutate<T>(@noescape f: T -> ())(x: T) -> T {
f(x)
return x
}
I added a version that can work with struct
s too:
func mutate<T>(inout x: T, @noescape f: inout T -> ()) {
f(&x)
}
Unfortunatelly, it doesn't work with |>
and closure has to be declared as accepting an inout
argument:
mutate(&nameLabel.frame) { (inout f: CGRect) in
f.size.width = 666
}
I put it in my doodles repo. I'll try to keep that version up to date with what I use "in production".
I'll report here if I have any bigger thoughts on the subject. :v:
This adds a variant of the forward application operator for
T -> ()
functions, allowing you to strap side effects to an expression instead of having to drop into an imperative style. While this definitely has some 🙊 factor (and isn't technically forward application?), it really nicely handles a somewhat common desire when dealing with Cocoa's objectiveyness: expressing instances that have a few customized properties as a single expression.For example, check out how great declaring lazy properties can be:
Or how about inlining customized objects as parameters:
It's also useful for stuff outside of object setup (though this strikes me as a practice that could get messy fast). Here's adding logging to an expression that's being returned, without having to change
return e
intolet result = e; log(result); return result
... or remember how to do this with Xcode breakpoints:Things I'm not sure about:
Void
instead ofT
orT
instead ofVoid
and get unintended behavior? Using something slightly different (|>>
or||>
maybe?) would let the compiler help us out.So basically, everything(?).
Thoughts?
(Also, totes apologies if just dropping into your project and hollering "Hey, what do you think of this?" with a slightly long rambly pr is a faux pas... or just rude)