koka-lang / koka

Koka language compiler and interpreter
http://koka-lang.org
Other
3.16k stars 153 forks source link

How to make these functions generic? #286

Closed tjpalmer closed 5 months ago

tjpalmer commented 1 year ago

Here's a sample program, and my question is how to avoid duplicating perform and really-perform for types string and list<string>. Note that with-mode properly abstracts over things, but it receives a callback for that.

effect env
  val mode: string

fun count(items: list<a>): int
  items.length

fun really-perform(task: list<string>): <console, env> int
  println(mode ++ ": " ++ task.show)
  task.count

fun really-perform(task: string): <console, env> int
  println(mode ++ ": " ++ task.show)
  task.count

fun perform(task: list<string>): <console, env> int
  really-perform(task)

fun perform(task: string): <console, env> int
  really-perform(task)

// TODO Why not this?
// fun with-mode(new-mode: string, action: () -> <|e> a): <env|e> a
fun with-mode(new-mode: string, action: () -> <env|e> a): <|e> a
  with val mode = new-mode
  action()

fun main(): console ()
  with val mode = "safe"
  println(
    perform("something").show ++ " " ++
    with-mode("faster") { perform("reliable") }.show ++ " " ++
    perform(["again"]).show
  )

Bonus question in my TODO above. I expect to need to say that with-mode definitely uses effect env, since it says with val mode = ..., but instead I'm required to put it on the callback action, which surprises me.

For further reference, this program is for comparison with other languages in this repo.

tjpalmer commented 1 year ago

Also for reference, if I try making a generic perform:

fun perform(task: a): <console, env> int
  really-perform(task)

I get this error:

dyn.kk(16, 3): error: no function really-perform is defined that matches the argument types
  context      :   really-perform(task)
  term         :   really-perform
  inferred type: ($a, ...) -> ...
  candidates   : really-perform: (task : string) -> <console,env> int
                 really-perform: (task : list<string>) -> <console,env> int

I presume that's expected by y'all, but it shows an example wrong direction I've been down.

And I purposely made the function bodies for perform and really-perform exact copies for the two types.

TimWhiting commented 5 months ago

With the latest koka you can do the following:


effect env
  val mode: string

fun count(items: list<a>): int
  items.length

fun perform(task: a, ?count: a -> int, ?show: a -> string): <console, env> int
  println(mode ++ ": " ++ task.show)
  task.count

fun with-mode(new-mode: string, action: () -> <env|e> a): e a
  with val mode = new-mode
  action()

fun main(): console ()
  with val mode = "safe"
  println(
    perform("something").show ++ " " ++
    with-mode("faster") { perform("reliable") }.show ++ " " ++
    perform(["again"]).show
  )

As far as this question goes:

// Why not this?
// fun with-mode(new-mode: string, action: () -> e a): <env|e> a
fun with-mode(new-mode: string, action: () -> <env|e> a): e a

The action requires the env effect, but the body of with-mode handles it, so with-mode doesn't have to have any env handler in it's context necessarily. Imagine them as exception handlers: the signature for with-env says that action raises an error of type env, and with-mode handles that exception - it's not going to be caught any higher up. If with-mode raised an exception (env) prior to installing the handler for env then it would also need env in it's context.

In your case you do have a handler outside of with-mode all the time - which means the polymorphic type e where you call with-mode will be unified with env and possibly more effects, but it isn't necessarily the case that you have to call with-mode when there is already a handler outside of it. If you did want that to be the case you would use with override val mode = ....

I'm going to close this since the original issue is solved. If you have further questions or issues don't hesitate to create a new issue or a new discussion topic on the discussions tab.