These are chat archives for ramda/ramda

15th
May 2015
Michael Chang
@mflux
May 15 2015 00:51
hey all, is there a R.forEach for Objects? similar to what you'd expect underscore's _.each which runs a function over each element of an object?
I'm eyeing R.mapObj however that doesn't seem to do the correct behavior
Michael Chang
@mflux
May 15 2015 01:00
ah never mind! i figured it out

instead of

R.mapObj( entity.addComponent, components );

I have to do :

R.mapObj( entity.addComponent.bind( entity ), components );
that way it will have the correct "this" when addComponent is called!@
Brandon Wilhite
@JediMindtrick
May 15 2015 01:05
@jethrolarson Thanks for the good read.
Scott Sauyet
@CrossEye
May 15 2015 01:28
@mflux: that sounds a bit like abuse of mapObj, though. Are you using the return from it or simply modifying entity? If it's the latter, then I would find it an odd use of map.
Scott Christopher
@scott-christopher
May 15 2015 01:34
@mflux: Probably not as efficient as hand-rolling your own, but you can build it using something like:
var forEachObj = R.curry((fn, obj) => R.forEach(R.apply(fn), R.toPairs(obj)));
Scott Christopher
@scott-christopher
May 15 2015 01:40
which can be used like:
forEachObj((k,v) => console.log(k, v), { a: 1, b: 2 });
Scott Sauyet
@CrossEye
May 15 2015 01:52
@scott-christopher: Very nice!
Scott Christopher
@scott-christopher
May 15 2015 01:54
I find R.apply and R.toPairs tend to go quite nicely together for a number of different scenarios.
Scott Sauyet
@CrossEye
May 15 2015 01:55
@paldepind: Not silly at all. Quite interesting. But useful mostly for a set of types that will not be added to, since every dispatched function would have to be updated.
That is, even if you had something like Type.add(Shape, {Triangle: [Point, Point, Point]}), you have no (clean) way of updating area.
Michael Chang
@mflux
May 15 2015 05:11
@scott-christopher looks good, though I wonder why R doesn't just have a forEachObj
out of habit I started using underscore mixed with ramda and boy is that confusing to read...
it turns out I can just use R.values( obj ) and R.forEach that
Scott Christopher
@scott-christopher
May 15 2015 05:32
Yeah, though you won’t get the keys provided to your iterator function.
If you don’t need them, then it’s probably clearer.
Simon Friis Vindum
@paldepind
May 15 2015 06:20
@CrossEye I'm glad it's not that silly. When I did it at first it felt a bit ridiculous to implement a form of types in a library. I'm not sure I completely understand. Is the issue that it's not possible to add a new type and define it's area function without modifying the existing area function?
Hardy Jones
@joneshf
May 15 2015 08:04
it's the expression problem
you're forced to tackle the other dimension of it when you use adt's
rather than the normal dimension in regular js
but since it's solved, you can implement a solution for it, and no harm no foul.
the obvious solution using adt's as such, is to use final interpreters
Hardy Jones
@joneshf
May 15 2015 08:10
there's plenty of research and paper and tutorials on that, but the punchline is to use multiple dispatch.
Shape.prototype.area = function() {
  return Type.case({...}, this);
};
var area = function(x) { 
  return x.area();
};
or something...
at least, the punchline in js
Hardy Jones
@joneshf
May 15 2015 08:16
see also object algebras

also, I guess I didn't really explain the expression problem.

When you have an algebraic data type like that, it's very easy to add another function that operates on that type (since nothing has to be updated, only added), but very hard to add another type (since everything has to be updated as you identified and @CrossEye pointed out).

Whereas when you use inheritance (prototypal or otherwise), it's very easy to add another type (since nothing has to be updated, only added), but very hard to add another function that operates on that type (since everything has to be updated).

Simon Friis Vindum
@paldepind
May 15 2015 09:15
@joneshf Thanks for the explanation! :) I can definitely see the inflexibility there. But don't you have the same problem in Haskell if you define a algebraic data type like this:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
Hardy Jones
@joneshf
May 15 2015 09:33

yep, and the solution is still final interpreters:

class Area a where
    area :: a -> Double

instance Area Shape where
    area (Circle r x y) = ...
    area (Rectangle x y z w) = ...

And then if you need another type with area, you just add that.

data Triangle = Triangle Point Point Point

instance Area Triangle where
    area (Triangle a b c) = ...
though to really make it extensible, you'd separate out each case in the Shape. and give more functions in the Area class.
Something to construct a thing, other things to operate on it, etc.
Basically make a DSL for working with shapes.
Simon Friis Vindum
@paldepind
May 15 2015 09:40
Shouldn't Triangle somehow be a part of the Area Shape instance?
Simon Friis Vindum
@paldepind
May 15 2015 09:50
You have both data Shape = Circle Float Float Float | Rectangle Float Float Float Float and data Triangle = Triangle Point Point Point. So Triangle hasn't really extended the Shape type?
Raine Virta
@raine
May 15 2015 15:54
hmm, docs update broke alfred-ramda-workflow, ramdajs.com/docs/#invoker format no longer works because version is always needed. I can fix this by adding version number to ramda-json-docs format, but should the latest always be available without version number? @buzzdecafe
Hardy Jones
@joneshf
May 15 2015 15:56

Sorry. It was late, I didn't really explain well. Though it's kind of a poor example to explain the idea on, since there's only one operation in the language so far. But the idea is that instead of viewing Shape as the type you're trying to extend, you look at the operations that you use on the data and that becomes your type to extend. This makes for a DSL you can express (in the haskell case as a type class, in the js case you can stick things on the prototype). Then you program only with this DSL:

doubleArea :: Area a => a -> Double
doubleArea x = 2 * area x

sumAreas :: Area a => [a] -> Double
sumAreas xs = sum $ map area xs

smallest :: Area a => [a] -> a
smallest xs = minimumBy (comparing area) xs

This way, you don't have to rewrite anything when you add another type to Area. Yet, you still retain the ability to add more functions generically--by adding another type class, or sticking more things on the prototype.

So if you wanted to add some linear transformation functions:

class Scale a where
    scale :: Double -> a -> a

instance Scale Shape where
    scale s (Circle r x y) = ...
    scale s (Rectangle x y z w) = ...

instance Scale Triangle where
    scale s (Triangle a b c) = ...

class Rotate a where
    rotate :: Radian -> a -> a

instance Rotate Shape where
    ...

instance Rotate Triangle where
    ...

Once again, nothing has been modified that was already written. Only new stuff added, and all the old code still works.
And you can add even more functions without having to modify old stuff.

minimumArea :: (Area a, Scale a) => Double -> a -> a
minimumArea a x = if area x < a then {- some sort of computation involving `scale` -} else x

Does that make more sense?

Hardy Jones
@joneshf
May 15 2015 16:03
translated to js that'd be like:
function doubleArea(x) {
  return 2 * x.area();
}

function sumAreas(xs) {
  return R.sum(R.map(R.invoke('area'), xs));
}

function smallest(xs) {
  return R.minBy(R.invoke('area'), xs);
}

function minimumArea(size, x) {
  return x.area() < size ? /* some sort of computation involving `scale` */ : x;
}
Simon Friis Vindum
@paldepind
May 15 2015 19:43
@joneshf Yes. It's a great explanation. I've found out that what I've implemented in silly-type is union types. I understand type classes and how different instances can implement the functions that the class specify. But in your last examples you still have Shape and Triangle as two different types when Triangle really should have been a value of Shape? I still don't see a way to add a new value to a type once defined like this:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
Hardy Jones
@joneshf
May 15 2015 19:48
That's right, you can't
Simon Friis Vindum
@paldepind
May 15 2015 19:51
:(
Hardy Jones
@joneshf
May 15 2015 19:51
why sad?
you don't lose any expressive power
you just have to pivot your viewpoint :)
Simon Friis Vindum
@paldepind
May 15 2015 19:53
But then I can't define a type Shape and extend it afterwards. That's what @CrossEye mentioned initially: "That is, even if you had something like Type.add(Shape, {Triangle: [Point, Point, Point]}), you have no (clean) way of updating area."
Hardy Jones
@joneshf
May 15 2015 19:58
right, because area as it was originally defined was an initial interpreter.
and there's no easy way to extend adt's and initial interpreters without rewriting something.
The initial part sort of means you've already decided on a data type to work with. Whereas the final part sort of means that you're waiting on the data type to be given to decide how to work with it.
in a hand-wavy sense.
Hardy Jones
@joneshf
May 15 2015 20:07
Like R.map is a final interpreter because it seems to have the type: Functor f => (a -> b) -> f a -> f b, whereas R.of is an initial interpreter because it has the type: a -> [a].

you can't make R.of work with other data types without rewriting the implementation (because it's an initial interpreter), but I can write:

function Id(x) {
  this.x = x;
}
Id.prototype.map = function(f) {
  return new Id(f(this.x));
}

And use it with R.map without changing anything in the implementation (because it's a final interpreter).

var i3 = new Id(3);
R.map(R.inc, i3); //=> Id(4);
Hardy Jones
@joneshf
May 15 2015 20:15
So the "type" i'm working with is the DSL for Functor. I can add more actual types (Identity, Maybe, List, etc) to this DSL, and never update the old code. But I can also add more functions to this "type" (the DSL for Functor) (void, replace, etc) and also never update the old code.
it's true you cannot modify an old ADT after the fact without also modifying all the initial interpreter style functions you also wrote (this is the bulk of the expression problem). But you also don't need to if you think about the problem from a different angle.
Hardy Jones
@joneshf
May 15 2015 20:23
if it sounds like a lot of trouble, that's because the problem was hard to solve :)
using either adts as the primitives or using objects as the primitives
Simon Friis Vindum
@paldepind
May 15 2015 20:27
So an initial interpreter is one that works on a type? And a final interpreter works on a type class?
Hardy Jones
@joneshf
May 15 2015 20:33
pretty much
i hand-waved a bit there, but basically.
and of course, these aren't the only solutions to the problem
Hardy Jones
@joneshf
May 15 2015 20:39
the point is that you haven't lost (or gained unfortunately) any expressive power, just a different view on the same stuff. And since it's a good library, you should keep going with it!
i know that if you put it on npm i'd use it for stuff
Simon Friis Vindum
@paldepind
May 15 2015 20:51
It's on npm now as union-type-js.
I've just added support for using the constructors for the native types in type specifications:
var Action = Type({Shout: [Number, String]});
I'll have to think a bit more about type classes :)
Hardy Jones
@joneshf
May 15 2015 20:54
Sorry, i didn't mean to suggest that you NEEDED to add anything to it, just that you haven't lost anything if you really wanted to go find it again.
also, thanks!
Scott Sauyet
@CrossEye
May 15 2015 20:55
This is very exciting. I can think of several places where this would have come on handy. I can't wait to try it.
Simon Friis Vindum
@paldepind
May 15 2015 21:04
@joneshf I didn't understand it that way :) I'm glad you shared you opinion and took the time to explain!
@CrossEye :) :D
Michael Hurley
@buzzdecafe
May 15 2015 22:01
@joneshf thanks for the lesson. what should i be reading to gain better understanding of this stuff?