[racket] Pattern matching define macro

瀏覽次數:25 次
跳到第一則未讀訊息

Brian Adkins

未讀,
2014年7月12日 中午12:55:342014/7/12
收件者:Racket Users List
I'm porting more Haskell code to Racket as a learning exercise. When I got to this line:

isPos (r,c) = elem r [0..4] && elem c [0..r]

I first wrote this:

(define (is-pos r c) (and (member r (lgen 0 4))
(member c (lgen 0 r))))

where lgen is:

(define (lgen m n) (build-list (+ 1 (- n m)) (λ (x) (+ x m))))

I think (lgen 0 r) is a reasonable alternative to [0..r], and the minor additional length of the Racket version is fine.

I then decided that I may prefer to a list of row/col instead of individual args and bumped into a need for destructuring a list, so I wrote this macro:

(define-syntax defpat
(syntax-rules ()
[(_ (fn pat) b1 b2 ...)
(define fn (match-lambda [pat b1 b2 ...]))]))

which allows:

(defpat (is-pos (list r c)) (and (member r (lgen 0 4))
(member c (lgen 0 r))))

The fact that this is such a common operation and I couldn't find anything built-in makes me think that I may be missing something. Is this a reasonable solution? Are there better alternatives?

I suppose a better name might be in order since it's not matching one of several patterns; in this case, it's really just for destructuring a list more concisely.

I'm still blown away by how easy it was to mold Racket closer to what I wanted. I've just barely begun learning macros, but syntax-rules made this pretty easy.

I think the only thing that sticks out is the "list" function, but that seems like a reasonable sacrifice given the flexibility it allows for in more complicated patterns.

Thanks,
Brian

--
Brian Adkins
Lojic Technologies, LLC
http://lojic.com/


____________________
Racket Users list:
http://lists.racket-lang.org/users

Matthias Felleisen

未讀,
2014年7月12日 下午1:19:032014/7/12
收件者:Brian Adkins、Racket Users List

On Jul 12, 2014, at 12:53 PM, Brian Adkins wrote:

> I'm porting more Haskell code to Racket as a learning exercise. When I got to this line:
>
> isPos (r,c) = elem r [0..4] && elem c [0..r]
>
> I first wrote this:
>
> (define (is-pos r c) (and (member r (lgen 0 4))
> (member c (lgen 0 r))))
>
> where lgen is:
>
> (define (lgen m n) (build-list (+ 1 (- n m)) (λ (x) (+ x m))))
>


Have you considered turning your Haskell code into prefix and leaving it otherwise alone (mostly):

#lang racket

;; -----------------------------------------------------------------------------
;; prelude

(module+ test (require rackunit))

(define-syntax-rule
(= (f x ...) expression)
(define (f x ...) expression))

(define (.. low high) (range low high))
(define (elem r rng) (cons? (member r rng)))

;; -----------------------------------------------------------------------------
;; Haskell to Racket
;; isPos (r,c) = elem r [0..4] && elem c [0..r]

((isPos r c) . = . (and (elem r [.. 0 4]) (elem c [.. 0 r])))

;; -----------------------------------------------------------------------------
;; some tests

(module+ test
(check-true (isPos 1 0))
(check-true (isPos 2 1))
(check-false (isPos 1 2))
(check-false (isPos 5 1)))

Okay, the line is still a bit wider but that's because of extra spaces -- Matthias

Jens Axel Søgaard

未讀,
2014年7月12日 下午1:22:412014/7/12
收件者:Brian Adkins、Racket Users List
Hi Brian,

I think you want define/match.

(define/match (is-pos l)
[((list r c)) (and (member r (range 0 4))
(member c (range 0 r)))])


(is-pos '(2 5)) ; => #f
(is-pos '(2 1)) ; => '(1)

/Jens Axel

--
--
Jens Axel Søgaard

Neil Van Dyke

未讀,
2014年7月12日 下午2:40:242014/7/12
收件者:Brian Adkins、Racket Users List
Complementary to the suggestions that Matthias and Jens Axel made, also
remember that Racket has a very different evaluation model than Haskell.

Consider reworking the use of the "and" form and everything within it:
instead, test "r" and "c" for "integer?", and then use "<=" and ">=" to
determine whether in range.

Otherwise, you're constructing lists of numbers on each call to
"is-pos". This might be practically OK in this particular case, since
the lists will always be small, but it's not something you'd want to do
if the lists could ever be huge. And I'd say it's not really idiomatic
Racket for purposes of your learning exercise.

Remember that Racket is an imperative language; Racket is not Haskell,
nor is Racket a mathematician. :)

Neil V.

Brian Adkins wrote at 07/12/2014 12:53 PM:
> I'm porting more Haskell code to Racket as a learning exercise. When I got to this line:
>
> isPos (r,c) = elem r [0..4] && elem c [0..r]
>
> I first wrote this:
>
> (define (is-pos r c) (and (member r (lgen 0 4))
> (member c (lgen 0 r))))
>
> where lgen is:
>
> (define (lgen m n) (build-list (+ 1 (- n m)) (λ (x) (+ x m))))

[...]

Brian Adkins

未讀,
2014年7月12日 下午4:19:592014/7/12
收件者:Racket Users List
Thanks. I did look at define/match but:

(defpat (is-pos (list r c))
(and (member r (lgen 0 4))
(member c (lgen 0 r))))

seems nicer to me than:

(define/match (is-pos l)
[((list r c)) (and (member r (lgen 0 4))
(member c (lgen 0 r)))])

for two reasons:

1) It's odd to me to specify the l argument, and then never refer to it.
2) The syntax of the former seems less "noisy".

I can see the advantage of define/match when you have more than one pattern, and I think there's a disadvantage in introducing too many macros that are close to existing, standard, ones, but I *think* this is going to be a common enough situation for me that being slightly more concise and not creeping to the right may be worth it. I'll hold the opinion loosely though until I have a bunch more code reading/writing under my belt.

Brian

Brian Adkins

未讀,
2014年7月12日 下午4:20:272014/7/12
收件者:Racket Users List
Thanks for the advice Neil. I'm a little confused by your "Racket is an imperative language" statement though - it seems quite "functional" to me, which was one of the attractions.

The Haskell code was written a few years ago, as a relative newbie, as an exercise in emphasizing readability and conciseness, as well as showing how it can be close to mathematical notation, hence:

elem r [0..4] && elem c [0..r]

vs.

r >= 0 && r <= 4 && c >= 0 && c <= r

In other words, I wanted to express that r should be an element of the set [0..4], etc. w/o worrying about performance.

It does seem to be a tendency in Haskell code - for example, the quicksort implementation is beautiful, but doesn't use one of the key ideas of swapping in place. It makes for a great demo, but wouldn't be something you'd want in a performance-sensitive section of code.

I am interested in learning the idioms of the Racket community (especially before contributing code), but for this exercise, I wanted to see how close I could get to the original. A separate exercise would be to solve the problem from scratch in a more idiomatic style. It's a toy program (solves the Cracker Barrel pegboard puzzle):

http://blog.lojic.com/2011/03/10/cracker-barrel-peg-board-puzzle-in-haskell/

I didn't want to spend the time on a fresh look, so a transliteration was what I was shooting for. I'm not going to try and code Haskell in Racket, or convert Racket to Raskell :) But, I do like the fact that Racket seems flexible enough to allow me to steal some good ideas from other languages and incorporate them into my Racket code.

Brian

Neil Van Dyke

未讀,
2014年7月12日 下午5:20:182014/7/12
收件者:Brian Adkins、Racket Users List
Brian Adkins wrote at 07/12/2014 04:19 PM:
> Thanks for the advice Neil. I'm a little confused by your "Racket is an imperative language" statement though - it seems quite "functional" to me, which was one of the attractions.
[...]

Racket can be used as a functional language, so long as one remembers
that it's mostly an algorithmic language with an imperative and
sequential evaluation model, with not much obvious magic.

I often see Racket code that (among other problems) looks like the
programmer sometimes thinks they are using a declarative language, or
code that would be more defensible if Racket were doing lazy
evaluation. What should be O(1) becomes O(n), O(n) becomes O(n^2),
constant cost per n increases orders of magnitude unnecessarily, etc.

I think that Racket's closure application is pretty easy to understand
if people are sat down and walked through it. I suspect people get
confused by having lots of different models thrown at them, or they
kinda skip over the evaluation model introductory info, or they're
exposed to the evaluation model very early on (while overwhelmed with
concepts) but then don't go back and re-learn the intro stuff later
(after they have internalized more context to anchor).

Neil V.

Asumu Takikawa

未讀,
2014年7月12日 下午6:18:502014/7/12
收件者:Brian Adkins、Racket Users List
On 2014-07-12 16:18:55 -0400, Brian Adkins wrote:
> 1) It's odd to me to specify the l argument, and then never refer to it.
> 2) The syntax of the former seems less "noisy".

I agree that the syntax can be noisy for simple cases.

The design rationale for having the header is to allow optional,
keyword, and rest arguments. Also it lets you avoid using an `and`
pattern to refer to the whole argument.

Just wanted to note that in case you or anyone else was wondering why
the seemingly redundant header is there.

Cheers,
Asumu

Daniel Prager

未讀,
2014年7月12日 下午6:21:072014/7/12
收件者:Brian Adkins、Racket Users List
Hi Brian

r >= 0 && r <= 4 && c >= 0 && c <= r

implies

0 <= c <= r <= 4

Or  using prefix and the variable-arity of <=:

(define (is-pos r c)
  (<= 0 c r 4))

which I think works well for clarity, concision, and efficiency.

Dan

Brian Adkins

未讀,
2014年7月12日 下午6:44:572014/7/12
收件者:Daniel Prager、Racket Users List
Very nice observation; I like it. I've always felt the variable-arity of lisp was a cool feature. I switched to a struct for the arg, but the following works:

(defpat (is-pos? (pos r c))
(<= 0 c r 4))

I probably won't keep my defpat macro, at least not in its present form (for one, it only handles a single arg); there's probably a balance between being concise and being general/flexible.

I finished the program, but it's very raw - I haven't tried to match the Racket style, be idiomatic, etc., just rushed through it to get a rough idea of how it compares:

https://gist.github.com/lojic/14aefacc29ab5a88fa98

I'll come back to it after I get some more experience and improve it. Despite the questionable utility of these exercises, I always run into something that causes me to learn something valuable about Racket.

Brian Adkins

未讀,
2014年7月12日 下午6:50:582014/7/12
收件者:Asumu Takikawa、Racket Users List
On Jul 12, 2014, at 6:17 PM, Asumu Takikawa wrote:

> On 2014-07-12 16:18:55 -0400, Brian Adkins wrote:
>> 1) It's odd to me to specify the l argument, and then never refer to it.
>> 2) The syntax of the former seems less "noisy".
>
> I agree that the syntax can be noisy for simple cases.
>
> The design rationale for having the header is to allow optional,
> keyword, and rest arguments. Also it lets you avoid using an `and`
> pattern to refer to the whole argument.
>
> Just wanted to note that in case you or anyone else was wondering why
> the seemingly redundant header is there.
>
> Cheers,
> Asumu

In hindsight, I was probably being overzealous in trying to get rid of everything I didn't need for my simple example. Being able to reference the original argument will certainly be needed at times, and a couple extra [ ] chars is really not that big of a deal. Plus the advantage of someone else being able to look at my code and immediately understand define/match vs. some ad-hoc macro I whipped up is significant.

As I mentioned in another reply, I quickly ran into a limitation with my macro that I wouldn't have if I had used define/match. I'll wait until I have enough code to see what the real pain points might be.

Alexander D. Knauth

未讀,
2014年7月12日 晚上10:19:322014/7/12
收件者:Brian Adkins、racket users list

On Jul 12, 2014, at 6:43 PM, Brian Adkins <racke...@lojic.com> wrote:

> On Jul 12, 2014, at 6:19 PM, Daniel Prager wrote:
>
>> Hi Brian
>>
>> r >= 0 && r <= 4 && c >= 0 && c <= r
>>
>>
>> implies
>>
>> 0 <= c <= r <= 4
>>
>>
>> Or using prefix and the variable-arity of <=:
>>
>> (define (is-pos r c)
>> (<= 0 c r 4))
>>
>>
>> which I think works well for clarity, concision, and efficiency.
>>
>> Dan
>
> Very nice observation; I like it. I've always felt the variable-arity of lisp was a cool feature. I switched to a struct for the arg, but the following works:
>
> (defpat (is-pos? (pos r c))
> (<= 0 c r 4))
>
> I probably won't keep my defpat macro, at least not in its present form (for one, it only handles a single arg); there's probably a balance between being concise and being general/flexible.

Although define/match is definitely more powerful, if you want it, this would probably be a good version of what you want that would handle multiple arguments: (I’ll reply again if I get one to work with optional and/or keyword-arguments)
(define-syntax defpat
(syntax-rules ()
[(defpat (f arg-pat ...) body ...)
(defpat f (match-lambda** [(arg-pat ...) body ...]))]
[(defpat id expr)
(define id expr)]))

Alexander D. Knauth

未讀,
2014年7月13日 凌晨12:43:332014/7/13
收件者:Alexander D. Knauth、racket users list

On Jul 12, 2014, at 10:17 PM, Alexander D. Knauth <alex...@knauth.org> wrote:

>
> On Jul 12, 2014, at 6:43 PM, Brian Adkins <racke...@lojic.com> wrote:
>
>> I probably won't keep my defpat macro, at least not in its present form (for one, it only handles a single arg); there's probably a balance between being concise and being general/flexible.
>
> Although define/match is definitely more powerful, if you want it, this would probably be a good version of what you want that would handle multiple arguments: (I’ll reply again if I get one to work with optional and/or keyword-arguments)
> (define-syntax defpat
> (syntax-rules ()
> [(defpat (f arg-pat ...) body ...)
> (defpat f (match-lambda** [(arg-pat ...) body ...]))]
> [(defpat id expr)
> (define id expr)]))

Ok I just made a version of defpat that can handle multiple arguments, optional arguments, keyword-arguments, and optional keyword-arguments.
I also made a form called my-match-lambda that defpat uses to do this.
https://github.com/AlexKnauth/defpat
Also to do this I had to make it so that you have to use square brackets to specify optional arguments, otherwise it couldn’t tell between an optional argument and a match pattern.

Alexander D. Knauth

未讀,
2014年7月14日 凌晨12:05:462014/7/14
收件者:Brian Adkins、racket users list
I just found this, which has a lot of forms for just about everything I can think of (and more) that support things like match-patterns:
https://github.com/stchang/generic-bind

I haven’t tried it yet, but It looks pretty amazing.

It probably has everything you want.

Stephen Chang

未讀,
2014年7月15日 下午3:04:202014/7/15
收件者:Alexander D. Knauth、racket users list
> I just found this, which has a lot of forms for just about everything I can think of (and more) that support things like match-patterns:
> https://github.com/stchang/generic-bind
>
> I haven’t tried it yet, but It looks pretty amazing.
>
> It probably has everything you want.

Thanks for the kind words. :)

This was an experiment that tried to address a few questions that
repeatedly get asked on the mailing list:

1) that you can't destructure data structures at the binding site, and
2) that you need to manually define many different versions of each
binding construct (eg define, match-define, match-define-values, etc),
which inevitably means that some binding forms don't exist (eg
match-for/X forms)

Something like the expressivity problem for binding forms.

I tried to implement one "generic" version of each binding form that
would accept many different "binding instances", so every binding form
would be extensible by implementing the appropriate instance (eg, a
matching instance, a values instance)

Every Racket binding form should have a ~-prefixed version in my
library and my version should work as a drop-in replacement for its
Racket counterpart if you want to try it out.

Here's a link to the docs if you're interested in reading more:
http://stchang.github.io/generic-bind/generic-bind.html

(You can tell that it hasn't been updated in a while from the old css.)

Caveat: my macro-fu hasn't quite reached expert level so I'd love for
anyone to tell me where my code is bad.

That being said, I ran a large part of the Racket test suite using my
binding forms instead of the Racket ones so they should be usable in
most scenarios.

Alexander D. Knauth

未讀,
2014年7月15日 下午4:13:422014/7/15
收件者:Stephen Chang、racket users list

By the way I’m just wondering:

Is there any reason why you can’t just use match-patterns instead of using ($ match-pattern) ?

And is there anything like a “generic-binding-expander” that would act sort of like a match-expander?

And is there some kind of "generic-bind/renamed” module or something that provides everything from generic-bind except without the ~ in front?

Stephen Chang

未讀,
2014年7月15日 下午5:42:272014/7/15
收件者:Alexander D. Knauth、racket users list
> Is there any reason why you can’t just use match-patterns instead of using ($ match-pattern) ?

The idea was to support a generic interface so different instances
could be defined, but thus each instance needs it's own name. For
example, I need to differentiate when I'm binding a match pattern vs
binding multiple values.

I tried to define shortcuts for some match patterns like $list and I
suppose you could rename them to be the same as racket but I didnt
want to clash with racket's names. There's also define-match-bind to
create your own shortcuts.


> And is there anything like a “generic-binding-expander” that would act sort of like a match-expander?

No there isnt. It was a todo but I never got around to it. Did you
have something specific in mind?


> And is there some kind of "generic-bind/renamed” module or something that provides everything from generic-bind except without the ~ in front?

Good idea. Yes, in hindsight my choice of names was probably
suboptimal. I've incorporated your suggestion so now you can do
(require generic-bind/as-rkt-names), which drops all ~-prefixes
(except for ~vs). Thanks!

Alexander D. Knauth

未讀,
2014年7月15日 下午6:23:372014/7/15
收件者:Stephen Chang、racket users list

On Jul 15, 2014, at 5:40 PM, Stephen Chang <stc...@ccs.neu.edu> wrote:

>> Is there any reason why you can’t just use match-patterns instead of using ($ match-pattern) ?
>
> The idea was to support a generic interface so different instances
> could be defined, but thus each instance needs it's own name. For
> example, I need to differentiate when I'm binding a match pattern vs
> binding multiple values.
>
> I tried to define shortcuts for some match patterns like $list and I
> suppose you could rename them to be the same as racket but I didnt
> want to clash with racket's names. There's also define-match-bind to
> create your own shortcuts.

Ok that makes sense.

>
>
>> And is there anything like a “generic-binding-expander” that would act sort of like a match-expander?
>
> No there isnt. It was a todo but I never got around to it. Did you
> have something specific in mind?

No, I just thought that it would be really useful and wanted to know if it was there.
Also I was thinking I could use it to make gen-bind expanders for match-patterns if I need them.

Stephen Chang

未讀,
2014年7月15日 下午6:38:462014/7/15
收件者:Alexander D. Knauth、racket users list
> Also I was thinking I could use it to make gen-bind expanders for match-patterns if I need them.

There's a define-match-bind that consumes a match pattern and defines
a $-prefixed name for that specific pattern that can be used in
gen-bind positions. Does that help?

Alexander D. Knauth

未讀,
2014年7月15日 下午6:41:032014/7/15
收件者:Stephen Chang、racket users list

On Jul 15, 2014, at 6:37 PM, Stephen Chang <stc...@ccs.neu.edu> wrote:

>> Also I was thinking I could use it to make gen-bind expanders for match-patterns if I need them.
>
> There's a define-match-bind that consumes a match pattern and defines
> a $-prefixed name for that specific pattern that can be used in
> gen-bind positions. Does that help?

Oh. When I saw that I thought it was just for structs.
回覆所有人
回覆作者
轉寄
0 則新訊息