Compare commits
No commits in common. "f887be396badcb17ae210144ac25cd3d0dbc017b" and "1d91c5108489aea7b641aa35ad9557911625ff7e" have entirely different histories.
f887be396b
...
1d91c51084
9 changed files with 20 additions and 271 deletions
15
blog
15
blog
|
@ -284,21 +284,6 @@ multi MAIN(
|
||||||
$db.write: $db-dir;
|
$db.write: $db-dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
#| Update the posted-at time on a post to the current time
|
|
||||||
multi MAIN(
|
|
||||||
"post",
|
|
||||||
"now",
|
|
||||||
#| The id of the post to update the posted-at time
|
|
||||||
Int $id,
|
|
||||||
#| The path of the database directory
|
|
||||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
|
||||||
){
|
|
||||||
my $db = read-db $db-dir;
|
|
||||||
my $post = $db.posts{$id.Int};
|
|
||||||
$post.posted-at = DateTime.now;
|
|
||||||
$db.write: $db-dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
#| Create a new series
|
#| Create a new series
|
||||||
multi MAIN(
|
multi MAIN(
|
||||||
"series",
|
"series",
|
||||||
|
|
|
@ -16,7 +16,6 @@ IO::Path:D $.source
|
||||||
#| The time to display for the creation of the post
|
#| The time to display for the creation of the post
|
||||||
has
|
has
|
||||||
DateTime:D $.posted-at
|
DateTime:D $.posted-at
|
||||||
is rw
|
|
||||||
is required
|
is required
|
||||||
is json(
|
is json(
|
||||||
:to-json(*.Str),
|
:to-json(*.Str),
|
||||||
|
|
|
@ -47,6 +47,7 @@ sub site-header(BlogMeta:D $meta) is export {
|
||||||
sub header-link($name, $path, $icon) {
|
sub header-link($name, $path, $icon) {
|
||||||
a :href("$path"), [
|
a :href("$path"), [
|
||||||
icon $icon;
|
icon $icon;
|
||||||
|
' ';
|
||||||
span $name;
|
span $name;
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,7 @@ sub show-html($html) is export {
|
||||||
# Work around HTML::Functional automatically putting newlines between tags
|
# Work around HTML::Functional automatically putting newlines between tags
|
||||||
$out ~~ s:g/'</i>' \v+ '<span>'/<\/i><span>/;
|
$out ~~ s:g/'</i>' \v+ '<span>'/<\/i><span>/;
|
||||||
$out ~~ s:g/\v+ '</a>'/<\/a>/;
|
$out ~~ s:g/\v+ '</a>'/<\/a>/;
|
||||||
$out ~~ s:g/\s+ ',' \s+ '<span'/, <span/;
|
$out ~~ s:g/',' \v+ '<span'/,<span/;
|
||||||
# Fixup unit test divs
|
|
||||||
$out ~~ s:g/'<div class="unit-test">' \s* '<pre><code class="idris-code"> '/<div class="unit-test"><span><i class='bx bx-check-circle'><\/i>Unit Test<\/span><pre><code class="idris-code">/;
|
|
||||||
$out
|
$out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
```idris hide
|
```idris hide
|
||||||
module LessMacrosMoreTypes.Printf
|
module LessMacrosMoreTypes.Printf
|
||||||
|
|
||||||
import Data.List
|
%default total
|
||||||
import System
|
|
||||||
```
|
```
|
||||||
|
|
||||||
While C can provide convenient string formatting by having hideously memory
|
While C can provide convenient string formatting by having hideously memory
|
||||||
|
@ -12,19 +11,18 @@ unsafe variadics, and dynamic languages, like python, can do the same while
|
||||||
being memory safe by not being type safe, many type safe languages, such as
|
being memory safe by not being type safe, many type safe languages, such as
|
||||||
Rust, are forced to provide such functionality through the use of a macro.
|
Rust, are forced to provide such functionality through the use of a macro.
|
||||||
Dependently typed languages, like Idris, can provide a printf like formatting
|
Dependently typed languages, like Idris, can provide a printf like formatting
|
||||||
interface, while maintaining both memory and type safety, without the need for
|
interface, while maintaining both memory and type saftey, without the need for
|
||||||
the macro. We will explore this by implementing a simplified version of `printf`
|
the macro. We will explore this by implementing a simplified version of `printf`
|
||||||
in Idris from scratch.
|
in Idris from scratch.
|
||||||
|
|
||||||
This article is inspired by an exercise from chapter 6 of
|
This article is inspired by an exercise from chapter 6 of [Type Driven
|
||||||
[Type Driven Development with Idris](https://www.manning.com/books/type-driven-development-with-idris),
|
Development with
|
||||||
and is written as a literate Idris file, with the source available
|
Idris](https://www.manning.com/books/type-driven-development-with-idris), and is
|
||||||
[here](https://git.stranger.systems/thatonelutenist/website/src/branch/trunk/projects/Idris/src/LessMacrosMoreTypes/Printf.md).
|
written as a literate Idris file.
|
||||||
|
|
||||||
## Gameplan
|
## Gameplan
|
||||||
|
|
||||||
Our goal is to provide a printf function that can be called, much like it's C
|
Our goal is to provide a printf function that can be called, much like it's C equivlant, like so:
|
||||||
equivalent:
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> As this is a literate Idris document, and we haven't defined our `printf`
|
> As this is a literate Idris document, and we haven't defined our `printf`
|
||||||
|
@ -34,202 +32,14 @@ equivalent:
|
||||||
|
|
||||||
```idris
|
```idris
|
||||||
failing
|
failing
|
||||||
printf "%s %d %2d" "hello" 1 2
|
example_usage : String
|
||||||
|
example_usage = printf "%s %d %02d" "hello" 1 2
|
||||||
```
|
```
|
||||||
|
|
||||||
Idris lacks a dedicated facility for variadics, but we can call functions in
|
|
||||||
type signatures, Idris allows us to manipulate types as first class values, and
|
|
||||||
we can use the runtime values of previous arguments to the function we are
|
|
||||||
defining as arguments to our type-level function.
|
|
||||||
|
|
||||||
To get our variadic `printf` function, we can parse our format string into a
|
|
||||||
data structure, then pass that data structure into a type-level function that
|
|
||||||
calculates the rest of the type signature of our `printf` function based on its
|
|
||||||
contents.
|
|
||||||
|
|
||||||
## Parsing a Format String
|
## Parsing a Format String
|
||||||
|
|
||||||
First, we need a data structure to describe our format string. We define the
|
|
||||||
`Format` data type, with constructors for each of the format specifiers we will
|
|
||||||
be supporting, as well as a constructor to hold literal components.
|
|
||||||
|
|
||||||
```idris
|
|
||||||
data Format : Type where
|
|
||||||
||| A slot that should be filled in with a number
|
|
||||||
Number : (next : Format) -> Format
|
|
||||||
||| A slot that should be filled in with a number, padded to a certian number
|
|
||||||
||| of digits
|
|
||||||
PaddedNumber : (digits : Nat) -> (next : Format) -> Format
|
|
||||||
||| A slot that should be filled in with a string
|
|
||||||
Str : (next : Format) -> Format
|
|
||||||
||| A literal component of the format string that should not be interpolated
|
|
||||||
Literal : (literal : String) -> (next : Format) -> Format
|
|
||||||
||| The end of the format string
|
|
||||||
End : Format
|
|
||||||
```
|
|
||||||
|
|
||||||
We'll need to be able to parse numbers for our `PaddedNumber` constructor, so
|
|
||||||
we'll write a little helper function to handle that component of the parsing.
|
|
||||||
Simply keep pulling off characters, converting them to integers by shifting
|
|
||||||
their ordinal values.
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> On Idris 2's chez scheme backend, these are unicode ordinals, where the digit
|
|
||||||
> characters are in numerical order, but the values are backend dependent and
|
|
||||||
> this code is not guaranteed to work properly on other backends.
|
|
||||||
|
|
||||||
```idris
|
|
||||||
parseNumber : (xs : List Char) -> (acc : Nat) -> (Nat, List Char)
|
|
||||||
parseNumber [] acc = (acc, [])
|
|
||||||
parseNumber (x :: xs) acc =
|
|
||||||
if isDigit x
|
|
||||||
then let value = cast $ (ord x) - (ord '0')
|
|
||||||
in parseNumber xs (acc * 10 + value)
|
|
||||||
else (acc, x :: xs)
|
|
||||||
```
|
|
||||||
|
|
||||||
We'll also want another one to scoop up a literal into a `List Char`, continuing
|
|
||||||
until we hit the end of the string or the next `%`. This is optional, we could
|
|
||||||
have each literal consist of an individual char, but we will go ahead and group
|
|
||||||
them together.
|
|
||||||
|
|
||||||
```idris
|
|
||||||
parseLiteral : (xs : List Char) -> (List Char, List Char)
|
|
||||||
parseLiteral [] = ([], [])
|
|
||||||
parseLiteral ('%' :: xs) = ([], '%' :: xs)
|
|
||||||
parseLiteral (x :: xs) =
|
|
||||||
let (literal, rest) = parseLiteral xs
|
|
||||||
in (x :: literal, rest)
|
|
||||||
```
|
|
||||||
|
|
||||||
Parse our format string into our `Format` data structure. The specifics of the
|
|
||||||
parsing here aren't really material to the main point of this article, but we
|
|
||||||
use a basic pattern matching approach, calling into our helper functions as
|
|
||||||
appropriate.
|
|
||||||
|
|
||||||
```idris
|
|
||||||
parseFormat : (xs : List Char) -> Maybe Format
|
|
||||||
parseFormat [] = Just End
|
|
||||||
-- A `%` has to come before a specifier
|
|
||||||
parseFormat ('%' :: []) = Nothing
|
|
||||||
parseFormat ('%' :: (x :: xs)) =
|
|
||||||
if isDigit x
|
|
||||||
-- Invoke parseNumber to get our padding specifier
|
|
||||||
then let (digits, rest) = parseNumber (x :: xs) 0
|
|
||||||
in case rest of
|
|
||||||
-- A padding specifier has to come before something
|
|
||||||
[] => Nothing
|
|
||||||
('d' :: ys) => do
|
|
||||||
rest <- parseFormat ys
|
|
||||||
Just $ PaddedNumber digits rest
|
|
||||||
-- A padding specifier is only valid before a number specifier
|
|
||||||
(y :: ys) => Nothing
|
|
||||||
-- Parse as an unpadded specifier
|
|
||||||
else case x of
|
|
||||||
'd' => do
|
|
||||||
rest <- parseFormat xs
|
|
||||||
Just $ Number rest
|
|
||||||
's' => do
|
|
||||||
rest <- parseFormat xs
|
|
||||||
Just $ Str rest
|
|
||||||
-- Any other character here is an invalid specifier
|
|
||||||
_ => Nothing
|
|
||||||
parseFormat (x :: xs) =
|
|
||||||
let (literal, rest) = parseLiteral (x :: xs)
|
|
||||||
in do
|
|
||||||
rest <- parseFormat rest
|
|
||||||
Just $ Literal (pack literal) rest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Calculating a Type From a Format String
|
## Calculating a Type From a Format String
|
||||||
|
|
||||||
```idris
|
|
||||||
PrintfType' : Format -> Type
|
|
||||||
PrintfType' (Number next) =
|
|
||||||
(num : Nat) -> PrintfType' next
|
|
||||||
PrintfType' (PaddedNumber digits next) =
|
|
||||||
(num : Nat) -> PrintfType' next
|
|
||||||
PrintfType' (Str next) =
|
|
||||||
(str : String) -> PrintfType' next
|
|
||||||
PrintfType' (Literal literal next) =
|
|
||||||
PrintfType' next
|
|
||||||
PrintfType' End = String
|
|
||||||
|
|
||||||
PrintfType : Maybe Format -> Type
|
|
||||||
PrintfType Nothing = Void -> String
|
|
||||||
PrintfType (Just x) = PrintfType' x
|
|
||||||
```
|
|
||||||
|
|
||||||
## printf
|
## printf
|
||||||
|
|
||||||
### With the Format Structure
|
|
||||||
|
|
||||||
```idris hide
|
|
||||||
left_pad : (len : Nat) -> (pad : Char) -> (str : String) -> String
|
|
||||||
left_pad len pad str =
|
|
||||||
let cs = unpack str
|
|
||||||
in if length cs < len
|
|
||||||
then pack $ replicate (len `minus` length cs) pad ++ cs
|
|
||||||
else str
|
|
||||||
```
|
|
||||||
|
|
||||||
```idris
|
|
||||||
printfFmt : (fmt : Maybe Format) -> (acc : String) -> PrintfType fmt
|
|
||||||
printfFmt Nothing acc =
|
|
||||||
\void => absurd void
|
|
||||||
printfFmt (Just x) acc = printfFmt' x acc
|
|
||||||
where
|
|
||||||
printfFmt' : (fmt : Format) -> (acc : String) -> PrintfType' fmt
|
|
||||||
printfFmt' (Number next) acc =
|
|
||||||
\i => printfFmt' next (acc ++ show i)
|
|
||||||
printfFmt' (PaddedNumber digits next) acc =
|
|
||||||
\i => printfFmt' next (acc ++ left_pad digits '0' (show i))
|
|
||||||
printfFmt' (Str next) acc =
|
|
||||||
\str => printfFmt' next (acc ++ str)
|
|
||||||
printfFmt' (Literal literal next) acc =
|
|
||||||
printfFmt' next (acc ++ literal)
|
|
||||||
printfFmt' End acc = acc
|
|
||||||
```
|
|
||||||
|
|
||||||
### With a Format String
|
|
||||||
|
|
||||||
```idris
|
|
||||||
printf : (fmt : String) -> PrintfType (parseFormat (unpack fmt))
|
|
||||||
printf fmt = printfFmt _ ""
|
|
||||||
```
|
|
||||||
|
|
||||||
We can call `printf` as expected, with the number of and types of the arguments
|
|
||||||
being determined by the provided format string:
|
|
||||||
|
|
||||||
```idris hide
|
|
||||||
-- @@test printf hello world
|
|
||||||
helloWorld : IO Bool
|
|
||||||
helloWorld = do
|
|
||||||
pure $
|
|
||||||
```
|
|
||||||
|
|
||||||
```idris
|
|
||||||
printf "%s %s%s %3d %d" "Hello" "world" "!" 1 23 == "Hello world! 001 23"
|
|
||||||
```
|
|
||||||
|
|
||||||
It will even fail to compile if you attempt to provide arguments to an invalid
|
|
||||||
format string, which we can demonstrate by trying to apply a padding modifier to
|
|
||||||
a string specifier:
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> `failing` blocks have an additional feature, they will trigger a compiler
|
|
||||||
> error if their contents _do_ compile successfully.
|
|
||||||
|
|
||||||
```idris
|
|
||||||
failing
|
|
||||||
printf "Hello %s %3s" "world" "!"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Working With Run-Time Format Strings
|
|
||||||
|
|
||||||
## Conclusions
|
|
||||||
|
|
||||||
### Why this API isn't really a great idea
|
|
||||||
|
|
||||||
<!-- Doesn't fail to compile if you don't pass any arguments -->
|
|
||||||
|
|
||||||
|
|
|
@ -55,16 +55,6 @@ I've written a few libraries and utilities for Idris 2 already:
|
||||||
provides comment-based unit testing capability. Currently written in Raku, I
|
provides comment-based unit testing capability. Currently written in Raku, I
|
||||||
hope to eventually rewrite this in idris.
|
hope to eventually rewrite this in idris.
|
||||||
|
|
||||||
### Raku
|
|
||||||
|
|
||||||
- [blog](https://git.stranger.systems/thatonelutenist/website)
|
|
||||||
|
|
||||||
The static site generator powering this blog. This is a horribly cursed tangle
|
|
||||||
of ill-thought-through raku code that is not meant to be reused. Feel free to
|
|
||||||
take a look and maybe take inspiration from it, or at the very least feel
|
|
||||||
better about your own just-get-it-done code, but don't let me catch you
|
|
||||||
forking it.
|
|
||||||
|
|
||||||
### Books
|
### Books
|
||||||
|
|
||||||
- [Idris by Highly Contrived Example](https://static.stranger.systems/idris-by-contrived-example/)
|
- [Idris by Highly Contrived Example](https://static.stranger.systems/idris-by-contrived-example/)
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
color: var(--fg-1);
|
color: var(--fg-1);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
border: solid 0.35rem;
|
border: solid 0.5rem;
|
||||||
margin-top: var(--box-margin-vert);
|
margin-top: var(--box-margin-vert);
|
||||||
margin-bottom: var(--box-margin-vert);
|
margin-bottom: var(--box-margin-vert);
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@
|
||||||
.warning .title p,
|
.warning .title p,
|
||||||
.caution .title p {
|
.caution .title p {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.note .title p::before,
|
.note .title p::before,
|
||||||
.tip .title p::before,
|
.tip .title p::before,
|
||||||
|
@ -36,7 +38,8 @@
|
||||||
.warning .title p::before,
|
.warning .title p::before,
|
||||||
.caution .title p::before {
|
.caution .title p::before {
|
||||||
font-family: 'boxicons' !important;
|
font-family: 'boxicons' !important;
|
||||||
font-size: 4rem;
|
font-size: 3rem;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Notes */
|
/* Notes */
|
||||||
|
@ -65,17 +68,17 @@
|
||||||
}
|
}
|
||||||
/* Warnings */
|
/* Warnings */
|
||||||
.warning {
|
.warning {
|
||||||
border-color: var(--yellow);
|
border-color: var(--orange);
|
||||||
}
|
}
|
||||||
.warning .title p::before {
|
.warning .title p::before {
|
||||||
content: "\eac5";
|
content: "\e9a3";
|
||||||
color: var(--yellow);
|
color: var(--orange);
|
||||||
}
|
}
|
||||||
/* Cautions */
|
/* Cautions */
|
||||||
.caution {
|
.caution {
|
||||||
border-color: var(--red);
|
border-color: var(--red);
|
||||||
}
|
}
|
||||||
.caution .title p::before {
|
.caution .title p::before {
|
||||||
content: "\eac6";
|
content: "\ee87";
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ code {
|
||||||
/* Styling for inline code blocks */
|
/* Styling for inline code blocks */
|
||||||
:not(pre) > code {
|
:not(pre) > code {
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styling for fenced code blocks */
|
/* Styling for fenced code blocks */
|
||||||
|
@ -19,30 +18,4 @@ pre > code {
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styling for unit tests */
|
|
||||||
.unit-test {
|
|
||||||
width: 90%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.55rem / 0.5rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.unit-test > span {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 0.55rem / 0.5rem;
|
|
||||||
}
|
|
||||||
.unit-test > pre {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.unit-test i {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,16 +78,6 @@ a:visited {
|
||||||
blockquote {
|
blockquote {
|
||||||
background-color: var(--bg-1);
|
background-color: var(--bg-1);
|
||||||
}
|
}
|
||||||
.unit-test {
|
|
||||||
background-color: var(--bg-1);
|
|
||||||
}
|
|
||||||
.unit-test > span {
|
|
||||||
background-color: var(--bg-2);
|
|
||||||
color: var(--fg-1);
|
|
||||||
}
|
|
||||||
.unit-test i {
|
|
||||||
color: var(--green);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Colorization for idris code blocks */
|
/* Colorization for idris code blocks */
|
||||||
code {
|
code {
|
||||||
|
|
Loading…
Add table
Reference in a new issue