Typeclasses for rendering different values as text for different audiences
The idea of a Show
typeclass is well established as a better alternative to using toString
, but it
is limited by its inability to distinguish different audiences. A fully-generic typeclass for
arbitrary different audiences is possible, but is usually better handled with different Text
types
too. Spectacular's compromise of distinguishing with Show
and Debug
typeclasses will be sufficient
for most purposes, and has the distinction that debug
will always provide some Text
value, while
show
will require an appropriate Show
instance to be provided.
- typeclass-based means of converting values to text
- defines the
Show
typeclass for text destined for end users - defines the
Debug
typeclass for text destined for the developer - extension methods
show
anddebug
will apply conversions using these typeclasses - the
show
method may fail if aShow
is not available - the
debug
method will fall back to aShow
method and thentoString
if noDebug
instance exists Debug
instances are generically derived for products and coproducts
All Spectacular terms and types are defined in the spectacular
package:
import spectacular.*
Given a value of some type, e.g. a value artifact
of type Artifact
, calling
artifact.show
will try to resolve a Show[Artifact]
typeclass and apply it
to artifact
. If such a contextual instance exists in scope the result will be
a Text
value; if it does not, then a compile error will be raised.
Calling artifact.debug
works similarly, but will never fail to compile:
First, a Debug[Artifact]
will be sought and applied. Failing that an
Encoder[Artifact]
will be used (see below). Then, a Show[Artifact]
will be
tried instead. Finally, if that fails, a Text
value will be created from
artifact.toString
. Since toString
is always defined (however inadequately),
the call always succeeds.
Note that this only applies when calling debug
on a value; summoning the
Debug
instance and applying it directly will still fail to compile if no
Debug
instance exists in scope.
Both Show
and Debug
provide very similar functionality, and for many types,
their implementations will be the same. They differ primarily in their intended
audience: Show
instances should produce textual output which is intended for
users, not programmers, while Debug
output is intended only for
programmers. The absence of a Show
typeclass for a particular type
corresponds to that type not being intended for user consumption, while a
programmer should always be happy to see some representation of a value,
however technical.
Thus, messages which are intended for end-users should use Show
and
"internal" messages should use Debug
.
Creating a given instance of Show
or Debug
is simple in both cases. Both
are single-abstract-method traits, so definitions are often take just a single
line. For example,
given Debug[Short] = short => t"Short(${short.toString})"
Since the debug
extension method will fall back to using a Show
instance
whenever a Debug
instance is not available, if the implementations are
identical, then it is sufficient just to provide a Show
instance.
Every product type, such as case classes, will have a default Debug
instance,
derived from calling debug
on each of its parameters. This will exist even if
some parameters do not have their own Debug
instance, since a Show
typeclass will be used as a fallback, and toString
as a last resort.
For example, the case class,
import anticipation.Text
class Id(id: String) { override def toString(): String = id }
case class Person(id: Id, name: Text, age: Int)
val person = Person(Id("jqm"), t"Jennifer", 61).debug
will produce the text,
Person(id=jqm,name=t"Jennifer",age=61)
using the toString
of Id
and the Debug
instances (provided by
Spectacular) for Text
and age
. Note that Text
's Debug
instance
produces pastable code, rather than simply returning the exact same Text
value, while its Show
instance does exactly that.
The values true
and false
often mean different things in different
scenarios, and without specifying, Spectacular will not presume any particular
show
serialization of a Boolean
value.
But by importing a BooleanStyle
value from the spectacular.booleanStyles
package, a style can be chosen, for example,
import turbulence.Out
import turbulence.stdioSources.jvm
import booleanStyles.yesNo
def run(): Unit = Out.println(true.show)
will print the text yes
.
Several BooleanStyle
options are provided in the booleanStyles
package,
yesNo
-yes
orno
onOff
-on
oroff
trueFalse
-true
orfalse
oneZero
-1
(true
) or0
(false
) and it is trivial to provide alternatives, for example:
import gossamer.*
given posNeg: BooleanStyle = BooleanStyle(t"+", t"-")
A further typeclass, Encoder
, also converts from a particular type to Text
,
but comes with a complementary Decoder
typeclass and has a particular intent:
it is intended to represent a canonical way to encode a value as a string, such
that that text may be decoded to restore the original value.
For example, a Url
(as defined in
Nettlesome) represents the
structure of a URL, but is encoded in a very standard way to a familiar
representation of a URL, such as https://example.com/
. This conversion should
be provided by an Encoder
instance, and a corresponding Decoder
should be
provided in order to parse the Text
representation of the URL back into a
Url
instance.
Encoder
s and Decoder
s are intended to be used by libraries which use text
as a serialization format. Jacinta
allows any type for which an Encoder
exists to be serialized to JSON, and any
type for which a Decoder
exists to be read from JSON.
Xylophone provides the same
functionality for XML and Cellulose
for CoDL.
While encoding to text will normally succeed in all cases, it's common for
decoder (or deserialization) to fail, if the input text is in the wrong format.
However, the API of Decoder
does not include any optionality in the signature
of its decode
method. That's because capabilities should be used to handle
failures, with greater flexibility. Given Decoder
instances should include
appropriate using
clauses to demand the capability to raise errors. If using
Contingency for error handling, that
implies a Raises
instance, while Scala's checked exceptions require a
CanThrow
instance for the exception type.
Spectacular is classified as maturescent. For reference, Soundness projects are categorized into one of the following five stability levels:
- embryonic: for experimental or demonstrative purposes only, without any guarantees of longevity
- fledgling: of proven utility, seeking contributions, but liable to significant redesigns
- maturescent: major design decisions broady settled, seeking probatory adoption and refinement
- dependable: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version
1.0.0
or later - adamantine: proven, reliable and production-ready, with no further breaking changes ever anticipated
Projects at any stability level, even embryonic projects, can still be used, as long as caution is taken to avoid a mismatch between the project's stability level and the required stability and maintainability of your own project.
Spectacular is designed to be small. Its entire source code currently consists of 439 lines of code.
Spectacular will ultimately be built by Fury, when it is published. In the meantime, two possibilities are offered, however they are acknowledged to be fragile, inadequately tested, and unsuitable for anything more than experimentation. They are provided only for the necessity of providing some answer to the question, "how can I try Spectacular?".
-
Copy the sources into your own project
Read the
fury
file in the repository root to understand Spectacular's build structure, dependencies and source location; the file format should be short and quite intuitive. Copy the sources into a source directory in your own project, then repeat (recursively) for each of the dependencies.The sources are compiled against the latest nightly release of Scala 3. There should be no problem to compile the project together with all of its dependencies in a single compilation.
-
Build with Wrath
Wrath is a bootstrapping script for building Spectacular and other projects in the absence of a fully-featured build tool. It is designed to read the
fury
file in the project directory, and produce a collection of JAR files which can be added to a classpath, by compiling the project and all of its dependencies, including the Scala compiler itself.Download the latest version of
wrath
, make it executable, and add it to your path, for example by copying it to/usr/local/bin/
.Clone this repository inside an empty directory, so that the build can safely make clones of repositories it depends on as peers of
spectacular
. Runwrath -F
in the repository root. This will download and compile the latest version of Scala, as well as all of Spectacular's dependencies.If the build was successful, the compiled JAR files can be found in the
.wrath/dist
directory.
Contributors to Spectacular are welcome and encouraged. New contributors may like to look for issues marked beginner.
We suggest that all contributors read the Contributing Guide to make the process of contributing to Spectacular easier.
Please do not contact project maintainers privately with questions unless there is a good reason to keep them private. While it can be tempting to repsond to such questions, private answers cannot be shared with a wider audience, and it can result in duplication of effort.
Spectacular was designed and developed by Jon Pretty, and commercial support and training on all aspects of Scala 3 is available from Propensive OÜ.
Something spectacular relates to a spectacle or show; in this case, the Show
typeclass.
In general, Soundness project names are always chosen with some rationale, however it is usually frivolous. Each name is chosen for more for its uniqueness and intrigue than its concision or catchiness, and there is no bias towards names with positive or "nice" meanings—since many of the libraries perform some quite unpleasant tasks.
Names should be English words, though many are obscure or archaic, and it should be noted how willingly English adopts foreign words. Names are generally of Greek or Latin origin, and have often arrived in English via a romance language.
The logo is a film reel, the medium for distributing a show.
Spectacular is copyright © 2024 Jon Pretty & Propensive OÜ, and is made available under the Apache 2.0 License.