robrix / Prelude

Swift µframework of simple functional programming tools
MIT License
409 stars 26 forks source link

Forward application variant for piping through `T -> ()` functions. #35

Closed kazmasaurus closed 6 years ago

kazmasaurus commented 9 years ago

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:

lazy var formatter = NSDateFormatter() |> {
    $0.dateFormat = "HH:mm"
    $0.timeZone = NSTimeZone(abbreviation: "GMT")
}

Or how about inlining customized objects as parameters:

let tauDay = calendar.dateFromComponents(NSDateComponents() |> {
    $0.month = 6
    $0.day = 28
    $0.year = 2031
})

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 into let result = e; log(result); return result... or remember how to do this with Xcode breakpoints:

return someExpression |> { logDebug($0) }

Things I'm not sure about:

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)

narfdotpl commented 9 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)
robrix commented 9 years ago

@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 Lenses 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!

kazmasaurus commented 9 years ago

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
}

The name

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.

Does this belong here?

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)

robrix commented 9 years ago

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.

narfdotpl commented 9 years ago

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 structs 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: