These are chat archives for ramda/ramda

15th
Dec 2018
Scott Sauyet
@CrossEye
Dec 15 2018 01:06 UTC

Well I for one am sorry to see this interesting discussion move to private. For the record, I hope that this channel can be used to discuss anything related to FP JS. I've learned a lot from the smart people who've come into any part of the Ramda community to add their two cents.

I've had no good vocabulary to express my distaste for TS when it comes to functional programming. None of the points have exactly surprised me, but I'm learning something by how they're expressed.

Well that's not quite true, that version of match, which I've written in many not-so-useful ways, was a revelation.
Asad Saeeduddin
@masaeedu
Dec 15 2018 01:12 UTC
Sorry to have moved the discussion out of here then, just didn't want to drown the room in non-Ramda related stuff
the short summary is we tried out a bunch of other formulations and it turns out TypeScript can't in fact express the type of match after all (correct me if that's not right @skatcat31)
Robert Mennell
@skatcat31
Dec 15 2018 01:15 UTC
it cannot with what you were looking for. With Generics it can get to an enforced version, but without inferred types and a proper type Map it can't be a dynamically typed function
so while you can use generics to enforce types and returns you can't enforce what values would be able to be outputs to the predicate based on the array
so even if you use symbols or defaults for not found, you'll still end up with the implicit type error instead of an explicit compiler error
Scott Sauyet
@CrossEye
Dec 15 2018 01:18 UTC
Thanks for the update.
Robert Mennell
@skatcat31
Dec 15 2018 01:19 UTC
so sadly the typing is all developer based and not user based
Scott Sauyet
@CrossEye
Dec 15 2018 01:19 UTC
What does that mean @skatcat31?
Robert Mennell
@skatcat31
Dec 15 2018 01:21 UTC
let test = match(x => x.a + 1, [1, x => x+1], [2, x => x-1]) // notice lack of default

test({a : 3}) // this should be a compiler error, not a runtime error
if the type system were able to be mapped over inferred values(and properly mapped) we could take the indexed values of the rest parameters and use them as enforcement types for the predicate
however I'm doing a deeper read into the advanced typing available with TS, but it looks like there is no way to do this currently with their implementation of type Map
Robert Mennell
@skatcat31
Dec 15 2018 01:28 UTC
I'm curious if a Map or some sort of keyed entry(Object, Map, Set, etc.) could be used and then the typing inferred from the keyed values? This does give it a more explicit footprint though
Asad Saeeduddin
@masaeedu
Dec 15 2018 01:29 UTC
a type system capable inferring the type of test to be { a: 0 | 1 } -> 1 | 2 | -1 | 0 would be quite a feat
Robert Mennell
@skatcat31
Dec 15 2018 01:29 UTC
@masaeedu succinctly inferring*
reading through the advanced types in TS it is possible, but at that point the typing is so long that it's just not worth it...
Asad Saeeduddin
@masaeedu
Dec 15 2018 01:30 UTC
i'm not sure i follow what this means. inference happens inside the compiler, by definition there is no user input of type annotation required
Robert Mennell
@skatcat31
Dec 15 2018 01:36 UTC
TS offers advanced types of Intersections, Unions, Type Guards and Differentiating Types, Aliases, Literal Types, Discriminated Unions, Index Types, Mapped Types(in)
and conditional types*
so using these you should be able to add that constraint, but the typing at that point is a little convoluted
hmm apparently TS offers a Partial, Readonly, Pick, and Record... I wonder if Pick would be good enough for this?
Scott Sauyet
@CrossEye
Dec 15 2018 01:41 UTC

I guess what I was thinking is that Hindley-Milner offers a pretty straightforward signature for something close to the above match function:

match :: (a -> b) -> [Pair a b] -> a -> b

It's not exact, because the function is actually variadic rather than accepting a list of Pairs, but it's quite close, and we could easily imagine a simple extension to HM to account for this.

It's this combination of expressivity and simplicity that seems to be lacking in TS.

Brad Compton (he/him)
@Bradcomp
Dec 15 2018 01:48 UTC
For the record, I wrote TS for the last six month and hated it. It couldn’t even handle some OOP patterns for reuse. I had an object that had to conform to an interface. All the methods had similar initial processing so I tried to write a decorator to take n the specific handling and return a method that conforms to the interface and handles preprocessing. Problem was that TS couldn’t infer thatI was@returning a method with the correct signature so I couldn’t use the decorator and still have the object conform to the interface.
Sorry, typing on my phone. No more laptop :worried:
Scott Sauyet
@CrossEye
Dec 15 2018 01:53 UTC
No more laptop? That's a bummer.
Robert Mennell
@skatcat31
Dec 15 2018 01:56 UTC
@Bradcomp I'm a little lost... did you mean you built a decorator to take in a function and return it with pre-processing to make the function conform?
(also condolences on the laptop...)
Brad Compton (he/him)
@Bradcomp
Dec 15 2018 01:58 UTC
Exactly. It was supposed to take a method that didn’t conform to the interface and decorate it to conform. An adaptor if you will
Robert Mennell
@skatcat31
Dec 15 2018 01:58 UTC
did it do that with a return signature conforming to the interface?
Asad Saeeduddin
@masaeedu
Dec 15 2018 02:02 UTC
@CrossEye It gets fairly complicated to accurately type in systems like Haskell as well. The problem isn't so much the angle brackets vs Hindley Milner I think (those are sort of just skins), it's that you start doing programming in earnest at the type level
Brad Compton (he/him)
@Bradcomp
Dec 15 2018 02:03 UTC
I tried it a bunch of ways, it was a decorator which already has some weirdness. But yeah I made the types line up, but it just took the original signature of the method as gospel and wouldn’t acknowledge the changes the decorator made to it
Asad Saeeduddin
@masaeedu
Dec 15 2018 02:03 UTC
for match for example I believe you'd need to do something like the following to actually capture the type we desire:
match :: (a -> Union (Map Fst xs)) -> xs -> a -> Union (Map (Return . Snd) xs)
Robert Mennell
@skatcat31
Dec 15 2018 02:06 UTC
@Bradcomp oof. Sounds like a pain
Brad Compton (he/him)
@Bradcomp
Dec 15 2018 02:07 UTC
Yeah when I asked about it in a TS discord they basically just said not to do that
Scott Sauyet
@CrossEye
Dec 15 2018 02:13 UTC
@masaeedu: Oh, right, that was incorrect, and your correction no longer looks simple!
Still, is this almost it (modulo variadic)?:
match :: (a -> b) -> [Pair b (a -> c)] -> (a -> c)
Asad Saeeduddin
@masaeedu
Dec 15 2018 02:19 UTC
the problem is that you have to be polymorphic in the list type
if you say [Pair b (a -> c)] you're throwing away the heterogeneity of the list
for example if you have that signature you just showed I could do match (id) [] 5
and then explode
the type of the list must constrain the type of the output of the function you're passing in
Robert Mennell
@skatcat31
Dec 15 2018 02:24 UTC
the type of the list must constrain the type and values of the output of the function you're passing in*
without that values it's much simpler
Scott Sauyet
@CrossEye
Dec 15 2018 02:29 UTC

I was suggesting that this would be a useful description of JS functions, not that we could port that into something like ML/Haskell. That match function is not necessary in Haskell, nor is it useful.

Perhaps it's that Ramda often deals with aspirational type signatures, not trying to be overprecise about what happens on any possible input but only expressing how, if you pass parameters of the given types, your results will be of the type expressed.

But just as likely, I'm still missing something here.

Asad Saeeduddin
@masaeedu
Dec 15 2018 02:30 UTC
do you mean the Pair b (a -> c) thing? What i'm trying to say is that if you just say match :: (a -> b) -> [Pair b (a -> c)] -> (a -> c), there's some unspoken constraints that aren't actually captured in the type signature
Scott Sauyet
@CrossEye
Dec 15 2018 02:31 UTC
But my problem has been that while I can do that in JS with a pseudo-HM syntax, I've struggled mightily to do the equivalent in TypeScript, similar to what @BradComp was describing.
What sorts of constraints?
Asad Saeeduddin
@masaeedu
Dec 15 2018 02:32 UTC
for example this type signature does not demand that b be a union of all the types of the first elements of the list (and nothing else), which is necessary to prevent the function from exploding at runtime
Robert Mennell
@skatcat31
Dec 15 2018 02:32 UTC

@CrossEye this is the closest I got using generics to infer

const match = <Row, Value, ReturnType>(p : (x : Value) => Row, ...c : Array< [Row, (x : Value) => ReturnType]>) : (x : Value) => ReturnType => {
  const cases = new Map(c);
  return x => cases.get(p(x))(x);
}

it still has the problem of not picking up on invalid values from the predicate though

let test = match(x => Math.random() > 0.5 ? "foo" : "bar", ["baz", x => x])
as you can see there's a 100% chance this is invalid at runtime, but TS won't catch that
Asad Saeeduddin
@masaeedu
Dec 15 2018 02:33 UTC
so while we can document: "don't do this! match(x => "foo")(["bar", id])(42)", the type signature itself doesn't make it impossible
with expressive enough type systems (and since we are using our imaginations after all, we can be as expressive as we like), you actually can formulate the type of a function that must be passed a function for which the return type is a union of all the types of the first elements of the list of pairs that is its second argument (try saying that five times fast)
Scott Sauyet
@CrossEye
Dec 15 2018 02:34 UTC
@skatcat31: I'm sure a great deal of my problem is a familiarity issue; I still skip reading a lot of the TS signatures I see, because they are either trivial and I don't need them, or they are unreadable to me. In either case, that little HM snippet I can grok.
Robert Mennell
@skatcat31
Dec 15 2018 02:36 UTC
there's to many implications in your response. Can you clarify what you mean? I get the HM part and understanding it no problem, but I'm confused in how it's related to the snippet I posted and the inherit problem to it
Scott Sauyet
@CrossEye
Dec 15 2018 02:38 UTC
@masaeedu: Are you simply pointing out that we can't use JS to know that all cases are covered? If so, then, agreed. JS obviously has no mechanism for that.
Asad Saeeduddin
@masaeedu
Dec 15 2018 02:40 UTC
I'm saying that we need more exotic features than simply Hindley Milner to actually express the type of match usefully (even if we're just writing these signatures for some imaginary type system in comments above our functions). Hindley Milner itself helps a lot with readability, but there's complicated value level behavior that needs complicated type level features to capture, and simply having that isn't sufficient
Scott Sauyet
@CrossEye
Dec 15 2018 02:41 UTC
@masaeedu: OK, and that's why I'm fairly happy writing in a dynamically typed language, and skipping most of those issues... until, of course, they end up biting me!
Asad Saeeduddin
@masaeedu
Dec 15 2018 02:42 UTC

this all sounds very abstract and handwavey, but putting it more concretely, if we say:

// :: (a -> b) -> [Pair b (a -> c)] -> (a -> c)
const match = f => cases => { ... }

and then someone comes along and tries to do:

match(x => x)([])(42)

then when they run the typechecker in their head so to speak, there's nothing that will tell them this is illegal. As far as they know (based on the type signature alone), this is perfectly valid

yeah, agreed
but another benefit of writing in a dynamically typed language is that if we've worked with interesting type systems where we can express a type signature that (mentally evaluated), tells you something is wrong with the invocation above, we can just add that as a comment. we can "use" the most powerful features of the most powerful type systems in one sense
Asad Saeeduddin
@masaeedu
Dec 15 2018 02:48 UTC
I definitely agree with you about the joy of just punting on these issues in a dynamically typed language. You don't need to start dealing with all this mind bending stuff; you can just write the value/function and it works fine. You don't need to go get a PhD in theorem proving to try and model it in the type system
Scott Sauyet
@CrossEye
Dec 15 2018 02:50 UTC

@skatcat31: My problem is in reading that function. I can work through it, but I'm constantly wondering what I've learned from it. Part of it is the TS common idiom of generally using explicit names (Row, Value, ReturnType) too often confuses me: "Wait, Row? As opposed to Column? Or does this have something to do with row polymorphism? And what was that again? And does that Value have to do with a Name/Value pair, or is it something else? I guess I understand ReturnType. It maps to... no wait, where was that first declared?"

But it's more than that. It's that I know little of this static type-checking actually checks my types. It's mostly a compile-time thing. Well, I often work in crazy environments, and not all the code that touches mine is TS, and the interface between the underlying untyped world and the typed gloss tends to get a bit fraught.

Robert Mennell
@skatcat31
Dec 15 2018 02:54 UTC
const match = <Row, Value, ReturnType> ...
could easily be rewritten as
const match = <Type1, Type2, Type3> ...
it is however confusing to read when you're used to the other type systems and you switch to one as... unintuitive as TS
could have sworn I had an unintuitive there the first time round
Scott Sauyet
@CrossEye
Dec 15 2018 17:35 UTC
Right, it's not just the mechanics of TS, but also how it tends to be used. In HM, generic types are written as a, b, c,... which are explicitly non-representative. While people could do this in TS, they don't. Perhaps it's with good reason, but I find that I'm with John Degoes here: Descriptive Variable Names: A Code Smell