Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make-parameter with non-unit converter function #409

Closed
robertlemmen opened this issue Mar 19, 2019 · 11 comments
Closed

make-parameter with non-unit converter function #409

robertlemmen opened this issue Mar 19, 2019 · 11 comments

Comments

@robertlemmen
Copy link

I am using make-parameter with a custom converter function like so:

(define pa1 (make-parameter 1 (lambda (x) (+ 10 x))))
(pa1) 
(parameterize ((pa1 2))
  (list
    (pa1) 
    (parameterize ((pa1 3))
      (pa1))
    (pa1)
  ))
(pa1) 

which gives me:

> > 11
> (12 13 22)
> 21

I don't think this is correct, I think it should be:

> > 11
> (12 13 12)
> 11

also note that if I remove the innermost parameterize, I get:

> > 11
> (12 12)
> 21

So the parameterize seems to affect the outer binding, that can't be right! Perhaps the converter function gets called at the wrong point? or perhaps in the wrong context so that it sees it's own previous value as an input?

@robertlemmen robertlemmen changed the title mak-parameter with non-unit converter function make-parameter with non-unit converter function Mar 19, 2019
@gwatt
Copy link
Contributor

gwatt commented Mar 19, 2019

Parameterize has to call the parameter in order to restore its previous value, which will invoke the converter function. Since your converter function modifies the value it's storing, every time you restore the previous value it will be increased by 10.

@qzivli
Copy link

qzivli commented Mar 19, 2019

In the design of R7RS, when the body of parameterize leave the dynamic environment, the previous values of the parameters should restored without passing them to the converter.

Will Chez Scheme follow this semantic?

@akeep
Copy link
Contributor

akeep commented Mar 19, 2019

Actually, R7RS states (in Section 4.26):

Note: If the conversion procedure is not idempotent, the results of (parameterize ((x (x))) ...), which appears to bind the parameter x to its current value, might not be what the user expects.

Since the conversion procedure in this case is not idempotent, Chez Scheme's semantics seem compatible with R7RS.

@robertlemmen
Copy link
Author

robertlemmen commented Mar 20, 2019

I don't share the opinion that the current behavior of chez is correct as per R7RS:

  • the "idempotent" paragraph is talking about the case where parameterize is called in a way that rebinds the parameter based on it's current/outer value, see the example quoted above: (parameterize ((x (x))) ...). This is an extreme and unusual use case, and quite different from the one at the top of this discussion, where the parameterize values are just that: simple values

  • If the current behavior is correct, then I can't think of any converter function besides (lambda (x) x) that can be used to any safe and positive effect, that can't have been the intention when support for it was defined in R7Rs

  • apart from the wording and intentions of R7Rs, what would you expect in the example above? What do you think a language user (as opposed to one implementing a runtime) would expecrt? The expectation that the previous value is restored "after" the parameterize expression is quite natural, as this is pretty much the whole reason for the existence of parameterize.

  • Not saying that these schemes are a definition of correctness in any way, but it may be interesting that chibi, guile and mit-scheme do behave as I originally expected

@gwatt
Copy link
Contributor

gwatt commented Mar 20, 2019

I read through the make-parameter and parameterize section of r7rs and saw this:

Then the previous values of the parameters are restored without passing them to the conversion procedure.

So, the initial expectation is correct, with respect to r7rs.

@akeep
Copy link
Contributor

akeep commented Mar 20, 2019

Let's leave the conversation about R7RS for a moment, which is somewhat moot since Chez Scheme is an R6RS implemntion not an R7RS implementation, and talk about the interface that function generated by make-parameter provides (at least in Chez Scheme).

The procedure produced by make-parameter in just a normal Scheme case-lambda that takes either no arguments or a single argument. When a conversion procedure is supplied (or as Chez Scheme refers to it a filter procedure), that procedure is always called when the parameter procedure is called with a single argument. This really restricts what parameterize can do with the parameter procedure. It can get its current value and it can set it, going through the conversion/filter function along the way.

This still admits many useful filter procedures and we use them fairly extensively. We are just restricted to the procedures that return the same value for value they return. This can be as simple as a procedure that converts its arguments into boolean values:

(lambda (x) (and x #t))

Or more complicated, such as one that expects a boolean or output-port and returns an input-port or false,

(lambda (x)
  (cond
    [(eq? x #t) (console-output-port)]
    [(eq? x #f) #f]
    [(and (output-port? x) (textual-port? x)) x]
    [else (error #f "expected boolean or textual output port" x)]))

This hints at our primary use of this, which is to restrict the values the parameter can take on:

(lambda (x)
  (unless (procedure? x) (error #f "expected procedure" x))
  x)

At any rate, not useless at all, though not designed with your use case in mind.

The advantage to this implementation is that it is very simple and gets the advantage that mutable cell that holds the value of the parameter is isolated to a single single source point in the program, with some protection around what values it might hold. An implementation like that in SRFI-39 provides the behavior you want, with the complication that the procedure produced by make-parameter has an additional style of call for use by parameterize to ensure the conversion function does not get called on the way out of parameterize.

It is possible, in fact, to get the behavior you want by using SRFI-39 in your program, instead of Chez Scheme's implementation of make-parameter and parameterize, with the caveat that Chez Scheme provided parameters do not provide the addition two-argument interface needed by SRFI-39's parameterize.

Back to the R7RS. My apologies on my misreading of this, in my quick scan I had missed the sentence:

Then the previous values of the parameters are restored without passing them to the conversion procedure.

That is what I get for scanning this quickly while I'm waiting on a test to finish at work.

@akeep
Copy link
Contributor

akeep commented Mar 20, 2019

I read through the make-parameter and parameterize section of r7rs and saw this:

Then the previous values of the parameters are restored without passing them to the conversion procedure.

So, the initial expectation is correct, with respect to r7rs.

Yep, missed that on my quick read through, should have been more thorough, my apologies.

@robertlemmen
Copy link
Author

thanks for the detailed explanation, very helpful! In fact I don't have an issue with the chez implementation but just stumbled over this because I am writing a toy scheme, and use chez as a reference in some cases.

I kinda understand how chez implements this, but there is one part that to me seems more like an implementation choice than a necessity: you are saying that the conversion is executed on the parameter function each time, but that parameter function is returned by make-parameter, which just gets an init value. so can it not use the init value each time rather than the parameter? That would result in the conversion/filter not getting called in a nested way...

@akeep
Copy link
Contributor

akeep commented Mar 20, 2019

No, the procedure returned by make-parameter is effectively this:

(let ([v (guard init)])                                                         
  (case-lambda                                                                  
    [() v]                                                                      
    [(u) (set! v (guard u))]))

Where guard is the conversion function, so the value of the parameter is stored in v and the only way to access that value is by calling the procedure without arguments and the only way to set that value is to call it with one argument, which goes through the guard function. You can see simple implementations of make-parameter and parameterize in CSUG Section 12.13. Parameters. parameterize then gets the current value on entry by calling the parameter with no arguments, and resets it to that value on exit by calling the version with an argument.

In order to support setting v without going through the guard you'd need some way to put a value in v without calling guard or some way to store the previous value of v and reset it on some kind of stimulus. SRFI-39 does this through the dynamic-lookup which effectively swaps a temporary storage cell for v in place of v within the body of the parameterize.

@dybvig
Copy link
Member

dybvig commented Mar 20, 2019

Parameters originated in Chez Scheme as a replacement for plain variables when customizing the behavior of a procedure or subsystem through some method other than passing extra arguments. For example, the *print-level* variable that controlled how much of a nested structure write and pretty-print print was replaced with a print-level parameter. A parameter is just an ordinary procedure with state. One "references" a parameter by calling it without arguments and "sets" the parameter by calling it with one argument, the new value.

One reason why parameters are superior to variables is that the new value can be checked when the parameter is set. For example, print-level can check that its argument is either false or a nonnegative exact integer. This means the value does not need to be checked at every reference point, which simplifies the code and speeds up the references, and it means that invalid variables are reported when the assignment occurs, which simplifies debugging.

Another is that a parameter can act upon the new value whenever it is set. For example, collect-trip-bytes is like a normal parameter but also sets a corresponding variable in C for use by the C portions of the storage-management system. In other words, it works like a database trigger that notifies C of each change.

A parameter can even do non-trivial things when referenced. For example, current-directory returns the current directory (obtained via getwd) when referenced and changes it (via chdir) when set.

The r7rs parameter mechanism is less expressive than Chez Scheme's in at least three ways:

  • One cannot set the value of a parameter except temporarily via parameterize. That is, changes with indefinite extent are not supported.

  • The filter (converter) does not have the opportunity to operate on values restored by parameterize. This rules out parameters like collect-trip-bytes that need to do something whenever the parameter is (re)set.

  • Parameters can be created only via make-parameter. This rules out parameters like current-directory that need to do something when read.

So Chez Scheme won't be adopting the r7rs semantics, outside of a possible future r7rs compatibility library.

In any case, the behavior described in the original post is correct. Other behaviors are also correct depending on the evaluation order of the arguments to list.

@robertlemmen
Copy link
Author

great discussion and feedback, thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants