sanctuary-js / sanctuary

:see_no_evil: Refuge from unsafe JavaScript
https://sanctuary.js.org
MIT License
3.03k stars 94 forks source link

Question about S.concat #709

Closed tureluren closed 3 years ago

tureluren commented 3 years ago

Hello, as a newbie in functional programming I can't get my head around something.

S.lte returns true if the second argument is less than or equal to the first. Which comes in handy when filtering data like in your example:

S.filter (S.lte (3)) ([1, 2, 3, 4, 5]) // [1, 2, 3]

So why does S.concat not work in the same fashion?

// Why does this not result in ["a*", "b*", "c*"] ?
S.map (S.concat ("*")) (["a", "b", "c"]) // ["*a", "*b", "*c"]

// Or when composing in pointfree style where data comes last.
// Why does this not result in "abc" ? Concat works like a prepend in this situation.
S.compose (S.concat ("c")) (S.concat ("b")) ("a"); // "cba"

Is this intentionally?

davidchambers commented 3 years ago

Hello, @rifraf93, and welcome to the community. :wave:

In Haskell, <> is an infix operator:

> "foo" <> "bar"
"foobar"

The lovely thing about <> is that one need not consider nesting:

> "foo" <> "bar" <> "baz"
"foobarbaz"

Sanctuary gives us two equally valid ways of expressing the above, both noisy:

> S.concat (S.concat ('foo') ('bar')) ('baz')
'foobarbaz'

> S.concat ('foo') (S.concat ('bar') ('baz'))
'foobarbaz'

In Haskell, infix operators can also be treated as regular functions. The function equivalent of <> is (<>):

> :type (<>)
(<>) :: Semigroup a => a -> a -> a

This is exactly the type of S.concat! Let's see how (<>) behaves:

> (<>) "foo" "bar"
"foobar"

The behaviour of S.concat is thus consistent with that of (<>).

Haskell supports sectioning:

> ("foo" <>) "bar"
"foobar"

> (<> "bar") "foo"
"foobar"

Sectioning permits partial application of either argument:

> map ("prefix-" <>) ["foo", "bar", "baz"]
["prefix-foo","prefix-bar","prefix-baz"]

> map (<> "-suffix") ["foo", "bar", "baz"]
["foo-suffix","bar-suffix","baz-suffix"]

Without sectioning, we must resort to using S.flip:

> S.map (S.concat ('prefix-')) (['foo', 'bar', 'baz'])
[ 'prefix-foo', 'prefix-bar', 'prefix-baz' ]

> S.map (S.flip (S.concat) ('-suffix')) (['foo', 'bar', 'baz'])
[ 'foo-suffix', 'bar-suffix', 'baz-suffix' ]

I usually prefer functions to operators, as operator precedence is a source of incidental complexity. In this case, though, I do miss sectioning!

tureluren commented 3 years ago

Thank you for your quick response @davidchambers.

Though, now i'm confused about S.lte. <= is also an infix operator in haskell. So following your reasoning about S.concat, which was very clear, leads me to believe that one should resort to using S.flip with S.lte as well:

// Should be [2, 3]
> S.filter (S.lte (2)) ([1, 2, 3])
[ 1, 2 ]

// Should be [1, 2]
> S.filter (S.flip (S.lte) (2)) ([1, 2, 3]);
[ 2, 3 ]

Since in haskell:

> filter (2 <=) [1, 2, 3]
[2, 3]

> filter (<= 2) [1, 2, 3]
[1, 2]

Using S.concat with S.flip is of course only a small effort, it just confused me a little bit.

Thank you

davidchambers commented 3 years ago

S.concat has two common use cases: prepending and appending. JavaScript does not support sectioning, so we are forced to favour one use case (arbitrarily). It will be necessary to flip the function about half the time, whichever use case we favour. The deciding factor is that S.concat ('foo') ('bar') evaluating to 'foobar' is the more natural of the two possibilities.

tureluren commented 3 years ago

That makes sense, thank you for explaining.