On Monday, April 1, 2019 at 5:55:40 PM UTC-5, Ben Bacarisse wrote:
> This is interesting, and I admire your bravery in wading into these
> shark-infested waters dressed only in C, so I hope you won't find these
> remarks overly negative.
I hear you. I knew the code wasn't perfect (I had only just gotten it to
work).
> First, I don't see any closures because I don't see any environments. I
> don't think that's a problem -- you may not need them -- but it affects
> the description and that affects the readers expectations.
That's a good point. I was following Hutton and Meijer as mentioned in a
previous thread. Their implementation of 'result' uses a closure, so that
was the mental association I had while researching and experimenting.
So I didn't even notice when I disappeared. I think it got lost in the
translation into PostScript. I got the PS version to work by dynamically
generating a procedure, and by doing that I was able to "hard patch" the
value directly into the code. And so there was no longer any need for an
environment (or variables).
When moving to C, I only needed the single value to be accessible to the
function and associated with that instance of the function. I briefly
entertained the idea of making association lists, and if a future function
needs more than one "closed" value then it's still an option. That might
make it more recognizable as a closure.
There's also a sort of "double indirection" in the code due to my process
of moving ideas from the FP paradigm to OO and then doing "fake" OOP in C.
> Secondly, a monad is very general. I see bind and return functions but
> they seem to be tied to parsers. Am I to read bind and return as a
> specific instance of a monad?
Yes. 'result' and 'bind' provide monadic behavior (just) for parsers. I think
there's also the makings of a list monad in there, too.
So, yes, there's not actually a general Monad that can be instantiated
with different types. And there's no closures. And of course, there never
was a lambda. I think the laziness is pretty real, though. I got ideas
from the classic papers.
https://www2.seas.gwu.edu/~rhyspj/spring11cs3221/lab8/henderson.pdf
http://www.cs.indiana.edu/pub/techreports/TR44.pdf
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.40.237&rep=rep1&type=pdf
> Some comments on the code...
>
> M Joshua Ryan <
luser...@gmail.com> writes:
> > Norah is my computer's name.
> >
> >
> > #include <ctype.h>
> > #include <stdlib.h>
> > #include <stdio.h>
> >
> > typedef union object *object;
>
> I'm not a fan of hiding a pointer like this. It tripped me up for a
> while. For the price of a space and a * you lose the ability to remind
> the reader that it's pointers all the way down. However, I'm used to
> this construct, so I got over my initial stumble pretty quickly. It's a
> small point.
Point taken. I keep on doing this. And it keeps creating (some) difficulty
for others. I really want to abstract away from the pointerness. Would it
help to make it "louder"? sthg like:
#define HIDDEN_POINTER_TO *
typedef union object HIDDEN_POINTER_TO object;
> > typedef enum { INT, LIST, SUSP, PARSER, OPER } tag;
> > union object { tag t;
> > struct { tag t; int i; } Int;
> > struct { tag t; object a, b; } List;
> > struct { tag t; void *v; object (*f)( void * ); } Susp;
> > struct { tag t; void *v; object (*f)( void *, object ); } Parser;
> > struct { tag t; void *v; object (*f)( void *, object ); } Oper;
> > };
>
> I would be tempted to make all of these last three take the same
> function pointer type (giving the suspension a dummy object), and to
> typedef that function (but not the pointer type!) so as to tidy all this
> up a bit.
That's definitely worth considering. An earlier (failed) draft had more
typedefs for functions (well, the pointer type, as you might have guessed).
But it got confusing to me. I gave them bad names and went down a fruitless
path of naming the functions after their signatures. So I had FZ for
suspensions ('Z'ero arguments), FO for operators (one 'O'bject argument),
and FOO for parsers.
> <cut constructors>
>
> > object at( object a ){ return a && a->t == SUSP ? at( a->Susp.f( a->Susp.v ) ) : a; }
> > object x( object a ){ return a && a->t == LIST ? a->List.a : NULL; }
>
> Why is there no at( a->List.a ) here? Do you know there is never a
> suspension here? Is that a safe assumption in general? (I really don't
> know -- just asking.)
In the PostScript version there never ended up being any suspensions
in the "car" of the list, but it probably would be good to handle it
to make the code more generally useful.
> > object xs( object a ){ return a && a->t == LIST ? a->List.b = at( a->List.b ) : NULL; }
>
> You are cleverly doing a sort of pauper's graph reduction with that
> assignment. It's a neat idea (in a sneaky sort of way), but I am used
> to the suspension being evaluated when a value is actually used. I hope
> you don't find this is ever one step too soon. If you did delay until
> actual last-minute use, you would not be able to replace the list node,
> so I see why you are doing it like this.
That also came from the PS code. Since all list access goes through the x() and
xs() functions, it was a convenient place to do the update. I have considered
adding comments to the parts where mutations happen. ///MUTATION!
append() and join() would then needs such comments, but I think bind() might
not since the mutations done by join() are entirely /internal/ in bind().
> > object parse( object p, object input ){
> > return p->t == PARSER ? p->Parser.f( p->Parser.v, input ) : NULL;
> > }
>
> I am out of time but for one last remark, and I can make it here though
> it applies to other functions too. You might find it more helpful, down
> the line, to replace the
>
> return check_type ? something : NULL;
>
> pattern with
>
> retutn check_type_or_fail(), something;
Do you mean '&&' instead of ','?
> In code like this, NULL is a perfectly fine value (it's like Lisp's nil)
> and, with all the checks for it, your code will usually just elegantly,
> and silently, stop processing when it pops up. That's been my
> experience at least...
>
> <cut more code I don't have time for>
Much obliged. I had intend to write a lengthy description and explain
some of the pieces in detail. But ... writing is hard. I was excited that
the thing actually worked.
I noticed in another thread, a question about seq() came up. I thought
it might be necessary to write this function in order to get bind()
to work. As the name might suggest, it performs a sequence of two
parsers. I did get it written shortly after posting.
object fprepend( void *v, object o ){
return cons( cons( v, x( o ) ), xs( o ) );
}
object prepend( object a, object b ){
return map( Oper( a, fprepend ), b );
}
object fseq( void *v, object o ){
object q = v;
return prepend( x( o ), parse( q, xs( o ) ) );
}
object seq( object p, object q ){
return bind( p, Oper( q, fseq ) );
}
// ... in main() ...
object r = seq( is_alpha(), is_alpha() );
print( parse( r, p ) ), puts("");
//}
The next exercise will be a regex parser which produces
a parser as a result.