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

Add basic string interpolation / string templates #743

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

dvdvgt
Copy link
Collaborator

@dvdvgt dvdvgt commented Dec 12, 2024

Fixes #722 and adds basic support for string templates.

There are a few caveats to keep in mind with the current initial implementation:

  • All arguments already have to be strings. It is basically just syntactic sugar so that you can write "${a.show} and ${b.show}" instead of a.show ++ " and " ++ b.show where a and b are some 'showable' values. Internally, however, this is just how it is desugared.
  • String templates cannot be used to match string literals in pattern matches
  • The generated JS code looks a bit weird:
    const v_r_897 = $effekt.println(((((((((("GET ") + (domain_0)))) + (((("/users/") + (user_0))))))) + (((("/resource/") + (('' + resourceId_0))))))));
    
  • If one forgets to call show on arguments which are not already of type string, the error messages are hideous and possibly confusing:
error
-There are multiple overloads, which all fail to check:
-Possible overload: effekt::println of type Bool => Unit
-  Expected String but got Int.
-  Expected Bool but got String.
-
-Possible overload: list::println of type List[Bool] => Unit
-  Expected String but got Int.
-  Expected List[Bool] but got String.
-
-Possible overload: array::println of type Array[Int] => Unit
-  Expected String but got Int.
-  Expected Array[Int] but got String.
-
-Possible overload: effekt::println of type String => Unit
-  Expected String but got Int.
-
-Possible overload: list::println of type List[Int] => Unit
-  Expected String but got Int.
-  Expected List[Int] but got String.
-
-Possible overload: effekt::println of type Byte => Unit
-  Expected String but got Int.
-  Expected Byte but got String.
-
-Possible overload: list::println of type List[String] => Unit
-  Expected String but got Int.
-  Expected List[String] but got String.
-
-Possible overload: list::println of type List[Double] => Unit
-  Expected String but got Int.
-  Expected List[Double] but got String.
-
-Possible overload: array::println of type Array[String] => Unit
-  Expected String but got Int.
-  Expected Array[String] but got String.
-
-Possible overload: effekt::println of type Ordering => Unit
-  Expected String but got Int.
-  Expected Ordering but got String.
-
-Possible overload: array::println of type Array[Bool] => Unit
-  Expected String but got Int.
-  Expected Array[Bool] but got String.
-
-Possible overload: effekt::println of type Unit => Unit
-  Expected String but got Int.
-  Expected Unit but got String.
-
-Possible overload: option::println of type Option[Int] => Unit
-  Expected String but got Int.
-  Expected Option[Int] but got String.
-
-Possible overload: option::println of type Option[Double] => Unit
-  Expected String but got Int.
-  Expected Option[Double] but got String.
-
-Possible overload: array::println of type Array[Double] => Unit
-  Expected String but got Int.
-  Expected Array[Double] but got String.
-
-Possible overload: effekt::println of type Double => Unit
-  Expected String but got Int.
-  Expected Double but got String.
-
-Possible overload: effekt::println of type Int => Unit
-  Expected String but got Int.
-  Expected Int but got String.
-
-Possible overload: option::println of type Option[Bool] => Unit
-  Expected String but got Int.
-  Expected Option[Bool] but got String.
-
-  println("GET ${domain}/users/${user}/resource/${resourceId}")

Apart from that, the changes are really lightweight because of the early desugaring in the parser.

I am looking forward to your feedback and ideas.

@jiribenes
Copy link
Contributor

This is a great first implementation, but I'm not sure that desugaring in parser is the best choice here :/
We talked about it at a meeting when #722 was created, but I don't remember what we agreed on.

Personally, I'd much rather see templates up until the transformation to core so that we can:

  • have reasonable error messages in typer
  • later on resolve typeclasses or something for the splices
  • possibly have a more performant impl (?)

@dvdvgt
Copy link
Collaborator Author

dvdvgt commented Dec 12, 2024

Personally, I'd much rather see templates up until the transformation to core so that we can:

  • have reasonable error messages in typer
  • later on resolve typeclasses or something for the splices
  • possibly have a more performant impl (?)

You raise some very good concerns and I completely agree. I will try to go the 'non-lazy route' and I actually completely thread through templates to core.

@jiribenes
Copy link
Contributor

Don't get me wrong, having a version is still super helpful; I truly have no idea how difficult it really is to do the thing I outlined above. But I hope it's worth it, especially wrt technical debt. :)

@b-studios
Copy link
Collaborator

b-studios commented Dec 12, 2024

I am not completely against desugaring in parser, but maybe we could actually desugar

"foo ${exp1} bar ${exp2} baz"

to something like:

splice(["foo ", " bar ", "baz"], [exp1, exp2])

this way, as a user I can still overload splice for types other than exp1 exp2: String :) Not that don't have enough overloaded functions already...

@dvdvgt
Copy link
Collaborator Author

dvdvgt commented Dec 12, 2024

I just pushed a prototype that threads through string templates all the way until code generation.

This has the added benefit that we remain somewhat flexible for later changes, as Jiří mentioned. Furthermore, we can re-use the string interpolation capabilities of each respective backend, e.g. currently the JS string templates are generated as template literals. As a trade-off, we need to add a TemplateStr AST node to Core, CPS and to each backend. As I have already heard, not everybody is a proponent of further expanding Core ;)

Note: I currently only fully implemented it for JS. If we come to an agreement that this is the right way to proceed, I will implement for the remaining backends.

(I already started working on this before Jonathan's remark and I'd rather discuss this implementation briefly before scrapping it right away :) )

@jiribenes
Copy link
Contributor

jiribenes commented Dec 12, 2024

I am not completely against desugaring in parser, but maybe we could actually desugar

"foo ${exp1} bar ${exp2} baz"

to something like:

splice(["foo ", " bar ", "baz"], [exp1, exp2])

this way, as a user I can still overload splice for types other than exp1 exp2: String :) Not that don't have enough overloaded functions already...

This relates to some strategy we'll need to come up with in order to represent custom string interpolators (which is very outside of the scope of this PR, I believe...)

Then I'd expect some desugaring like, for example:

val name = "Joe"
val age = 42

////

val s: SQL::Stmt = 
  sql"SELECT * FROM Person p WHERE p.last_name = ${name} AND p.age = ${age}"

// ~>

val s: SQL::Stmt = sql { // sql: { prog: () => Unit / { Splice[String], Splice[Int] } }: SQL::Stmt
  do literal("SELECT * FROM Person p WHERE p.last_name = ")
  do splice(name)
  do literal(" AND p.age = ")
  do splice(age)
}

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

Successfully merging this pull request may close these issues.

Basic string templates / interpolation in normal strings
3 participants