busterjs / referee-combinators

Combinators goes assertion
0 stars 1 forks source link

First attempt on a bind assert #15

Open johlrogge opened 11 years ago

johlrogge commented 11 years ago

first draft, need to do more homework

I'm putting this up here for discussion. It certainly works. I'm just not sure it checks fulfills all criteria it should.

referee.add('bind', {                                                                                                                                  
   assert: function(actual, assertion1, assertion2) {                                                                                                  
      return when(assertion1.raw(actual),  assertion2.raw);
   } 
});  

The code including tests are in my develop branch in referee-combinators.

Remember that the referee-combinators adds a wrapper around the assertion so that you can do this:

var ca = combinators.assert;
var cr = combinators.refute;

ca.bind(ca.greater(4), ca.bind(cr.equals(8), ca.less(12)));

The above creates an assert that accepts actuals between 4 and 12 except for 8.

My feeling is that this assert should only appear under combinators... perhaps we need to support that?

meisl commented 11 years ago

I'd suggest to call it just what it is: binary and. For why NOT bind plz see this post in #13. Wasn't aware of this issue, sorry.

ca.bind(ca.greater(4), ca.bind(cr.equals(8), ca.less(12))); The above creates an assert that accepts actuals between 4 and 12 except for 8.

To be honest, this actually left me wondering for quite a while: I was like "what the HELL!? does this make any sense at all?!" My problem was to overlook that tiny little r in cr.equals(8), and "see" an a instead...

Not really sure what that means, maybe only that I should get some sleep. But possibly also that abbreviating

var ca = combinators.assert;
var cr = combinators.refute;

is NO good advice. Better have a more prominent assert vs refute in your face.

meisl commented 11 years ago

And hey, it's without doubt an important building block ~> +1 :)

"Sugar" like

inRange(4, 12).except(8)

or so is definitely conceivable.

johlrogge commented 11 years ago

the cr ca issue is really a sidetrack. I didn't want to write assert/refute to be clear about that they are not the same though I think that is probably how I would use them. In stead of assigning buster.assert to assert... but hey, sidetrack. Still good input for the examples.

I disagree with you about the name and while logical and can be used to chain statements together it is not exactly the same as just chaining them together. While and would not be much more than an alias for bind. I do agree that bind is ambiguous in the JS domain and that may be reason to change the name later.

I'm currently working under the assumption that what we are doing is pretty much the same things as is done in parser combinators. It is obviously not exactly the same but until I understand the differences well I'd rather adopt the current terminology in that domain rather than to think hard about what different name would be most appropriate in our domain. As you said in #13 we currently don't know enough about our domain. We can start out with the assumption that they are totally different or exactly the same (as a starting point). I have started out to assume that they are conceptually exactly the same. Coming up with different names on top of exploring the domain is too much of a cognitive load for me right now. Therefore the next combinator choice will not be called or (atleast initially).

We can always change the terminology when we know more. This should be done before we think we have a good first public release.

As a sidenote I think and will be different from bind regarding the error message that is generated. Bind is invisible, it is just "glue".

As you pointed up it is a good building block and one can add sugar on top of it. I'm seeing bind as very much an internal affair. While I don't think it should be off limits to an end user I think it should probably not be the first acquaintance the end user makes with combinators either. I'm thinking that structure (may that name soon rest in peace) is a better starting point.

Work now... I will get back to your other replies in other threads later. I really appreciate the considerable effort you spend trying to understand my ramblings and giving such interesting, relevant feedback. I'll try to make that part easier for you even when things are this exciting :)

meisl commented 11 years ago

Hmm, our approaches are indeed quite different. But I'm trying to adapat and you know what - not the worst what came out :)

I'll put this as kind of a dialogue between my "adapting self" and my "sceptic self". Intermissions by the latter appear as

citations (of the sceptic)

Let the play begin:

Anyways, let's just pretend we're in the "Assertion monad"!

1st problem: a monad must be a parameterized type - what is the a in Assertion a?!

Ignore! Ok, so we have bind for this monad, which in some way is logical and plus something...

2nd problem: the type of bind for Assertion a must be Assertion a -> (a -> Assertion b) -> Assertion b but all we see (at best) is a -> Assertion a -> Assertion a -> Promise ??

Ignore!

...and what is Promise ??? Is that a monad, too? (problem 3)

Quiet!! So we do have bind, by our decision! So let's go determine return (or result as it's called in the big paper)...

Errm... return a must be a left unit for bind, ie: return a >>= f = f a must hold...

Oh, that's easy: true is a unit for and. So with the bind at hand - return a must be constantly true.

Haha, return ignores its argument. Very clever monad indeed...

Shut up! It is, as we decided, not only a monad but a monad with zero and plus. Monadic plus will be called choice (or ++) and is somewhat like logical or, just as bind is somewhat like logical and. So let's see what monadic zero is here...

ERRM... zero must be the left and right unit element for ++, ie: zero ++ x = x and x ++ zero = x must hold...

Ha, easy enough - that's false! It's the unit for logical or. And or is also associative, ie: (p or q) or r = p or (q or r)!

was just about to say... Otherwise it wouldn't be a proper monad with plus and zero.

HA! And this zero (false) is also a zero element for our bind (and) - left and right! Ie: false and p = false and p and false = false. What are you saying now?!

Nice. But not essential for a monad with zero and plus.

So we're done, aren't we?

Not really. You have not given answers to problems 1, 2 & 3: [...]

The play did go on, but let's put a little break in here.

Can you guess how the sceptic will elaborate on the three problems mentioned? Particularly re 2, what objections will he bring up against the "bind" at hand as being the monadic bind which allegedly adds something substantial to simple logical and?

Then, in the end, a figure named "Solomon" appeared on stage and did the final monologue. He could not cut through the whole of the Gordion knot (it wasn't Alexander). However, he did point out prospective directions, particular wrt problem 1, "what's the type parameter?". ...which made the other two problems look a lot less daunting...

meisl commented 11 years ago

Note: there's one intermission by the sceptic that might be read over to easily. But it is a crucial one:

Haha, return ignores its argument. Very clever monad indeed...

Plz give this an additional thought.

johlrogge commented 11 years ago

:) That's not adapting, that's demagogy :)

Before the play continues:

I'm not saying that the assertions are monads or that they will be exactly the same and monads in the mathematical sense.

The opponent should work harder to make the skeptic see the point that the observable behavior from outside will be similar as with combinators and mimicking the existing concepts in combinators will give us something concrete to reason about. It would be a disaster if we only had one shot to get the terminology right, luckily we don't have such a limitation.

He should also point out that there is no true or false in the code, those are inferred by the skeptic.

As for zero there is reject.

That and would be a misnomer for something that just combines two things. When the things are equals and less it looks and behaves like and. What if we do this: bind(message('too young to drink: '), less(18)). Does and still make sense?

Also, remember that the above will be wrapped by referee-combinator into simplified this:

function(left, right) {
    return function(actual) {
       return when(left.raw(actual)).then(right.raw);
    }
}

remember: I have not claimed that this is a monad. Some has suspected (wrongly?) that promises are monads . Perhaps we cannot implement some of the concepts in the paper because it's not but lets run into that or prove it whichever comes first. I feel I get into analysis paralysis and I need to see something concrete in order to contribute anything to this.

I promise I will not be sensitive about correcting, changing, tossing or improving on the code. I just think I would understand better from that platform.

johlrogge commented 11 years ago

thinking out loud

While reading a bit more about parser combinators I was thinking about how to design asserts to mimic them. A key difference is that parser combinators work on streams of symbols. A parser can either:

An assert on the other hand deals with at least two kinds of complex objects {} and []. Also several primitives such as number, boolean etc. string could be considered complex or primitive, I'm thinking primitive (one could optionally make a parser that sees a string as a sequence of chars...).

what if we also view {} as a primitive? That is, in the same way a number can be greater than or equal to 0 an object could have a property matching assert a. In the same way an and can match several aspects of a number such as not being undefined, being 0 etc an and could match several aspects of a {}, some of those aspects could be drilling down into a property and recursively parse a [] but on each level {} and even [] in a [] can be considered primitive to that stream [] roughly = stream.

So what does an assert return? An assert returns [ft, rest] where ft is a failure tree and rest is remaining values to check.

zero would return no values left to check (I think).

Now an interesting thought at least I think so: A difference between seq/bind and and is that seq/bind consumes values from the stream while anddoesn't. That means that andchecks several properties of the same value while seq/bind moves forward in the stream or halts completely.

I'm not sure exactly about the details but I think that this could be in the direction of not cheating and actually creating an assert monad... Time to sleep.

meisl commented 11 years ago

Firstly: Sorry for not replying for so long.

One of my problems is time, or rather the lack of. But there's another serious impediment. Believe me, I've been thinking about how to best put this, before and now. This time I'll put one bold - maybe provocative - sentence in front, and hope you won't be put off too much to read on:

_PLZ, free yourself from the parser analogy - it's about MONADS! (if at all)_

Ok, that is really a pretty bold headline. So here's to alleviate: I do see that you're thinking about differences between parser combinators / combinator parsers and assertions, and that is the way to go. As a matter of fact,

I think the elegance is because the parsers are monadic, and that is where we can "borrow" from. Alas, this means "homework". Learn about monads in general. Those parsers are just one specific instance of a much more fundamental concept. It might of course be that in the end we find some corresspondence between parsing and testing, but in order to really judge (NOT: guess) we'll have to understand the underlying concept (monads) first. This is the "homework" that I am currently working on.

Ok, this should not be "preaching" alone. So here's at least some specific comments:

:) That's not adapting, that's demagogy :)

Sure, I am trying to manipulate you. Was just a new form, but by now you should be able to guess where I'm trying to get you ;)

The opponent should work harder to make the skeptic see the point that [...]

Indeed!

What if we do this: bind(message('too young to drink: '), less(18)). Does and still make sense?

No. And I don't see how your bind, as proposed in the first post, would make anything reasonable out of such parameters. Not even their number matches. In fact it's because of this that I say it (as proposed in 1st post) should not be named bind.

I have not claimed that this is a monad.

Then why call it bind after all? What's wrong with a name that just describes what it really does - and what you know for sure it really does?

Some has suspected (wrongly?) that promises are monads .

Thanks for the link. My thinking is that when.js just almost, but not quite, implements a monad. However, the concept of promises (not exactly sure about the spec, Promises/A) is nothing but the error monad inside the continuation monad. Or maybe the other way round; just as far as I can go with my current knowledge...

More on your most recent stuff in a separate post. Plz hang on, they'll connect better to the "headline", promised. Cheers :)


[1] convince yourself by digging out a) when and why we came to talk about "combinators" in the first place and b) when and where one of those papers about combinator parsers was first mentioned.

meisl commented 11 years ago

Some links I'd like to share while doing my homework:

meisl commented 11 years ago

I promise I will not be sensitive about correcting, changing, tossing or improving on the code. I just think I would understand better from that platform.

This is a good point, I'm taking it as a hint, wrt to the "form" :) Plz give me a litte time to think about how to put it to practice.

johlrogge commented 11 years ago

Firstly: Sorry for not replying for so long.

No problem. I'm happy you reply at all. Whenever you have the time. No stress.

_PLZ, free yourself from the parser analogy - it's about MONADS! (if at all)_

I'll get back to this but it is not only about monads.

  • the connection to monadic parsers is rather incidental[1]

The first time I realized that parsers could be used to parse structures of anything not just text was years ago when I first played with parser combinators in scala. And later when I was using a library that wasn't (to the best of my knowledge) monadic, to parse a Lisp-grammar. Those memories were brought to life in recent discussions and I connected the dots to promises and the specific problem at hand: asserting in a promise stream. I have toyed with many different ways of doing this before and through the discussion it started to feel like it is the same problem.

  • admittedly, it's fascinating how elegant those combinator parsers are. But, to be honest and as of now, all we know is that we want something similarly elegant for assertions / testing. That's it. I mean really know, and wrt that it is no more!

Actually, the esencial part is still testing Promise/A based code. At least for me.

I think the elegance is because the parsers are monadic, and that is where we can "borrow" from. Alas, this means "homework". Learn about monads in general. Those parsers are just one specific instance of a much more fundamental concept.

You're right they are just a specific instance of a much more fundamental concept (trust me I do understand that). The particular instance however has a core set of operations that fits like a glove to interpreting structures of stuff (in our case objects, lists and primitives) I know this as strongly as you know that monads is what we should focus our attention to. To me monads is one way to build parsers but I actually do think that the parser analogy is what is helpful here. We may both be wrong. But trust me: I will get to monads, I'm very curious but for me that is also a matter of time and prioritization and to the best of my judgement they can way just a little bit longer while I arm myself with problems to look for solutions to.

It might of course be that in the end we find some corresspondence between parsing and testing, but in order to really judge (NOT: guess) we'll have to understand the underlying concept (monads) first. This is the "homework" that I am currently working on.

I don't see why there has to be a dictated order of things here. Problems can be solved top down or bottom up. I'm pretty sure monads are very relevant to our problem (I think that can be said for pretty much any problem). I'm not sure they are the obvious end to start in.

Sure, I am trying to manipulate you. Was just a new form, but by now you should be able to guess where I'm trying to get you ;)

To be honest: My time is also limited. It would help if you actually said where you're trying to get me. I think it would save us some time :) And I don't get why you feel you have to try to manipulate me?

No. And I don't see how your bind, as proposed in the first post, would make anything reasonable out of such parameters. Not even their number matches. In fact it's because of this that I say it (as proposed in 1st post) should not be named bind.

You're right that my version of bind does not support this. My point was that I'm interested in doing one thing after another. That and has this property is coincidental.

You're wrong however about the number of arguments. I left out actual which works just fine in referee-combinators. I'm starting to feel that this is something that has not been clear and perhaps makes our disconnect look larger than it really is (I'm aware that there are quite a lot missing for a proper monad, can't say I know how to fix that exactly, that requires some homework, I'll get to that but not now)

Then why call it bind after all?

Because it binds to things together so that they are bound. It doesn't do this in a monadic way though.

What's wrong with a name that just describes what it really does - and what you know for sure it really does?

Well it does: it binds two things together as one. seq or combine would have been better choices to begin with but and describes my intention terribly. I simply wanted to do two things, one after the other. In the next iteration add the possibility to alter the value in the first thing before it was passed on to the second thing.

Thanks for the link. My thinking is that when.js just almost, but not quite, implements a monad.

That sounds about right. (from what I understand). Perhaps it implements what we need?

My feeling is that you see great potential in the assertion-combinator concept and that you are a bit worried that I will run ahead and do stupid things before you have time to dedicate to this and it will be "too late". Am I right?

I don't see that we need to "go public". That this can remain experimental for quite some time. The only users will be us and we will be able to reflect and redo as we both learn more about monads and other things. I'm aiming to get something crude to work to get a feeling for what this can look like and what the potential is. Not to rush something out to the public.

I'm not sure that feels better but that is why I said I will not be sensitive about my code and we can rewrite and redo.

johlrogge commented 11 years ago

I found this article to bridge the haskell notation: http://blog.jcoglan.com/2011/03/05/translation-from-haskell-to-javascript-of-selected-portions-of-the-best-introduction-to-monads-ive-ever-read/

I'm thinking that compose is closer?

thinking out loud

Another thing I'm debating with myself: reject for failure and error or only for error. (basically use reject as an exceptional case as opposed to an assertion failure such as would be the result for equals(3, 4)).

With the latter approach then compose would be more appropriate right? Basically in the normal case we just resolve and decide whether a fail or success occurred by other means (like returning a pair of failure and an empty rest to mark an assertion failure).

johlrogge commented 11 years ago

I have been playing with more combinators lately. Believe it or not as a part of that I have even read a bit more (don't get too excited) about monads. From a monad perspective it is obvious why bind is horrible name :P. I renamed bind to compose (I think it is more appopriate even if it may not be 100% correct it is more correct for sure). I still make no claims utilizing monads, it is clearer though (to me) where they will be useful.

My current set of operations:

compose, choice, repeat, attr and next. Next is a bit special in the lot, it consumes the next item in an array and applies assertions to it. Since it consumes the next item and returns the remaining items it is easy to make assertions like assert.repeat(assert.next(assert.equals(3)), refute.equals([])).

The last refute.equals([]) means that it repat will continue until the actual is an empty array. Not very clean but it gets the job done for now. The meaning of the complete statement above is "assert that every value in the array is a 3".

I think attr should also consume by returning the value of the given attribute and not return the entire object. That way one can descend into an object structure using repeat.

I think bind (the monadic bind, not my made up version) could perhaps come in handy inside repeat for instance if we want to attach some data each iteration such as which index we are currently on for the purpose of creating great messages. Not sure about this though...

It also feels that the asserts should just be predicates while the "combinator stuff" does not belong on assert at all. It feels wrong for instance to have an assert.compose.

Time to sleep. Just wanted to share my progress (it's been pushed and is really ugly) and also deliver the great news that I finally found some time to start looking at monads.

johlrogge commented 11 years ago

Oh by the way, it feels like both attr and next has a blt more in common (based on previous conversations about predicates etc) than the current implementations show. I think more studying of monads will help turning this feeling into code.