Need help with contextual?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

propensive
242 Stars 24 Forks Other 147 Commits 14 Opened issues

Description

Statically-checked string interpolation

Services available

!
?

Need anything else?

Contributors list

# 131,766
Scala
Shell
generic...
macro
110 commits
# 262,519
Scala
reactjs
scalajs
Android
4 commits
# 453,016
Scala
Shell
CSS
build-t...
3 commits
# 164,239
Scala
jupyter
texture...
gif-ani...
3 commits
# 252,526
Scala
Shell
concurr...
CSS
2 commits
# 92,023
Scala
playfra...
cats-ef...
fp
2 commits
# 337,037
Shell
Scala
bigquer...
Google ...
1 commit
# 378,061
Scala
Ember
ember-a...
Shell
1 commit
# 3,157
imagema...
sass-fr...
splash
pipelin...
1 commit

GitHub Workflow

Contextual

Contextual makes it simple to write typesafe, statically-checked interpolated strings. Contextual is a Scala library which allows you to define your own string interpolators—prefixes for interpolated string literals like

url"https://propensive.com/"
—which determine how they should be checked at compiletime and interpreted at runtime, writing very ordinary user code with no user-defined macros.

Features

  • user-defined string interpolators
  • introduce compile-time failures on invalid values, such as
    url"htpt://example.com"
  • compile-time behavior can be defined on literal parts of a string
  • runtime behavior can be defined on literal and interpolated parts of a string
  • types of interpolated values can be context-dependent
  • simple type-based parsing for interpolated values
  • shorthand
    Verifier
    class for defining runtime and compiletime behavior together

Getting Started

A simple example

We can define a simple interpolator for URLs like this: ```scala import contextual._

import language.experimental.macros

case class Url(url: String)

object UrlInterpolator extends Interpolator {

type Out = Url

def contextualize(interpolation: StaticInterpolation) = { val [email protected](_, urlString) = interpolation.parts.head if(!checkValidUrl(urlString)) interpolation.abort(lit, 0, "not a valid URL")

Nil

}

def evaluate(interpolation: RuntimeInterpolation): Url = Url(interpolation.literals.head) }

implicit class UrlStringContext(sc: StringContext) { def url(expressions: String*) = macro Macros.contextual[UrlInterpolator.type] ```

and at the use site, it makes this possible:

scala> url"http://www.propensive.com/"
res: Url = Url(http://www.propensive.com/)

scala> url"foobar" : error: not a valid URL url"foobar" ^

How it works

Scala offers the facility to implement custom string interpolators, and while these may be implemented with a simple method definition, the compiler imposes no restrictions on using macros. This allows the constant parts of an interpolated string to be inspected at compile-time, along with the types of the expressions substituted into it.

Note: Scala also allows the definition of string interpolators which make use of generics (i.e. accepting type parameters). Unfortunatly it's not possible to define a generic string interpolator using Contextual, and the macro would need to be defined manually in order to achieve that.

Contextual provides a generalized macro for interpolating strings (with a prefix of your choice) that calls into a simple API for defining the compile-time checks and runtime implementation of the interpolated string.

This can be done without you writing any macro code.

Concepts

Interpolator
s

An

Interpolator
defines how an interpolated string should be understood, both at compile-time, and runtime. Often, these are similar operations, as both will work on the same sequence of constant literal parts to the interpolated string, but will differ in how much is known about the holes; that is, the expressions being interpolated amongst the constant parts of the interpolated string. At runtime we have the evaluated substituted values available, whereas at compile-time the values are unknown, though we do have access to certain meta-information about the substitutions, which allows some useful constraints to be placed on substitutions.

The
contextualize
method

Interpolator
s have one abstract method which needs implementing to provide any compile-time checking or parsing functionality:
scala
def contextualize(interpolation: StaticInterpolation): Seq[Context]

The

contextualize
method requires an implementation which inspects the literal parts and holes of the interpolated string. These are provided by the
parts
member of the
interpolation
parameter.
interpolation
is an instance of
StaticInterpolation
, and also provides methods for reporting errors and warnings at compile-time.

The
evaluate
method

The runtime implementation of the interpolator would typically be provided by defining an implementation of

evaluate
. This method is not part of the subtyping API, so does not have to conform to an exact shape; it will be called with a single
Contextual[RuntimePart]
parameter whenever an interpolator is expanded, but may take type parameters or implicit parameters (as long as these can be inferred), and may return a value of any type.

The
StaticInterpolation
and
RuntimeInterpolation
types

We represent the information about the interpolated string known at compile-time and runtime with the

StaticInterpolation
and
RuntimeInterpolation
types, respectively. These provide access to the constant literal parts of the interpolated string, metadata about the holes and the means to report errors and warnings at compile-time; and at runtime, the values substituted into the interpolated string, converted into a common "input" type. Normally
String
would be chosen for the input type, but it's not required.

Perhaps the most useful method of the interpolation types is the

parts
method which gives the sequence of parts representing each section of the interpolated string: alternating
Literal
values with either
Hole
s (at compile-time) or
Substitution
s at runtime.

Contexts

When checking an interpolated string containing some DSL, holes may appear in different contexts within the string. For example, in a XML interpolated string, a substitution may be inside a pair of (matching) tags, or as a parameter to an attribute, for example,

xml"$content"
. In order for the XML to be valid, the string
att
must be delimited by quotes, whereas the string
code
does not require the quotes; both will require escaping. This difference is modeled with the concept of
Context
s: user-defined objects which represent the position within a parsed interpolated string where a hole is, and which may be used to distinguish between alternative ways of making a substitution.

This idea is fundamental to any advanced implementation of the

contextualize
method: besides performing compile-time checks, the method should return a sequence of
Context
s corresponding to each hole in the interpolated string. In the XML example above, this might be the sequence,
Seq(Attribute, Inline)
, referencing objects (defined at the same time as the
Interpolator
) which provide context to the substitutions of the
att
and
content
values.

Generalizing Substitutions

A typical interpolator will allow only certain types to be used as substitutions. This may include a few common types like

Int
s,
Boolean
s and
String
s, but Contextual supports ad-hoc extension with typeclasses, making it possible for user-defined types to be supported as substitutions, too. However, in order for the interpolator to understand how to work with arbitrary types, which may not yet have been defined, the interpolator must agree on a common interface for all substitutions. This is the
Input
type, defined on the
Interpolator
, and every typeclass instance representing how a particular type should be embedded in an interpolated string must define how that type is converted to the common
Input
type.

Often, it is easy and sufficient to use

String
as the
Input
type.

Embedding types

Different types are embedded by defining an implicit

Embedder
typeclass instance, which specifies with a number of
Case
instances how the type should be converted to the interpolator's
Input
type. For example, given a hypothetical XML interpolator,
Symbol
s could be embedded using,
scala
implicit val embedSymbolsInXml = XmlInterpolator.embed[Symbol](
  Case(AttributeKey, AfterAtt)(_.name),
  Case(AttributeVal, InTag) { s => '"'+s.name+'"' },
  Case(Content, Content)(_.name)
)
where the conversion to
String
s are defined for three different contexts,
AttributeKey
,
AttributeVal
, and
Content
. Whilst in the first two cases, the context changes, in the final case, the context is unchanged by making the substitution.

Attaching the interpolator to a prefix

Finally, in order to make a new string interpolator available through a prefix on a string, the Scala compiler needs to add an extension method to the

StringContext
type whose name is the same as the prefix to the interpolated string.

That definition must take the following form,

scala
implicit class UrlStringContext(sc: StringContext) {
  def url(expressions: String*): Url =
    macro Macros.contextual[UrlInterpreter.type]
}
for the interpolations to be "seen".

While the name of the implicit class,

StringContext
parameter and (of course) method name can vary, the parameter name,
expressions
, cannot, and it must be a repeated argument.

The

macro
invocation of
Macros.contextual
takes a single type parameter, which is the singleton type of the interpolation object.

Status

Contextual is classified as maturescent. Propensive defines the following five stability levels for open-source projects:

  • embryonic: for experimental or demonstrative purposes only, without guarantee of longevity
  • fledgling: of proven utility, seeking contributions, but liable to significant redesigns
  • maturescent: major design decisions broady settled, seeking probatory adoption and refinement of designs
  • dependable: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version
    1.0
    or later
  • adamantine: proven, reliable and production-ready, with no further breaking changes ever anticipated

Availability

Contextual’s source is available on GitHub, and may be built with Fury by cloning the layer

propensive/contextual
.
fury layer clone -i propensive/contextual
or imported into an existing layer with,
fury layer import -i propensive/contextual
A binary is available on Maven Central as
com.propensive:contextual-core_:2.0.0
. This may be added to an sbt build with:
scala
libraryDependencies += "com.propensive" %% "contextual-core" % "2.0.0"

Contributing

Contributors to Contextual are welcome and encouraged. New contributors may like to look for issues marked label: good first issue.

We suggest that all contributors read the Contributing Guide to make the process of contributing to Contextual easier.

Please do not contact project maintainers privately with questions, as other users cannot then benefit from the answers.

Author

Contextual was designed and developed by Jon Pretty, and commercial support and training is available from Propensive OÜ.

License

Contextual is copyright © 2016-20 Jon Pretty & Propensive OÜ, and is made available under the Apache 2.0 License.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.