Compare commits
25 Commits
configurin
...
c50529e44e
Author | SHA1 | Date | |
---|---|---|---|
c50529e44e | |||
fb9eea52a6 | |||
95e9b07f31 | |||
f8bd7e928d | |||
96f76a7751 | |||
a0640e1c0a | |||
8727fd33a6 | |||
869ad66b21 | |||
97afd713df | |||
a49af625bf | |||
7056ca822c | |||
7e290bee2f | |||
59cf230683 | |||
637fc7646b | |||
c62ff908af | |||
712b61059f | |||
2ead146d6a | |||
0ac70566cd | |||
809599d830 | |||
2e2f7b9be1 | |||
b0f0d15475 | |||
e951f7217f | |||
f7023b4431 | |||
08ad6fb469 | |||
b93aab5742 |
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
_site/
|
_site/
|
||||||
_cache/
|
_cache/
|
||||||
dist/
|
dist/
|
||||||
|
dist-newstyle/
|
||||||
|
result*
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: Contact
|
|
||||||
---
|
|
||||||
|
|
||||||
I live in Espoo Finland. You can contact me on any of the following services.
|
|
||||||
|
|
||||||
- **Email**: mats@rauhala.info
|
|
||||||
- **Slack**: masser@functionalprogramming.slack.com
|
|
||||||
- **IRC**: MasseR@freenode
|
|
||||||
- **Mastodon**: MasseR@mastodon.social
|
|
@ -1,3 +1,8 @@
|
|||||||
{ haskellPackages, haskell }:
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
haskell.lib.disableSharedExecutables (haskellPackages.callCabal2nix "site" ./. {})
|
with pkgs;
|
||||||
|
|
||||||
|
rec {
|
||||||
|
site = haskellPackages.callPackage ./site {};
|
||||||
|
rauhala-info = callPackage ./rauhala.info { site = site; };
|
||||||
|
}
|
||||||
|
64
flake.lock
generated
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"easy-hls": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1637250802,
|
||||||
|
"narHash": "sha256-/crlHEVB148PGQLZCsHOR9L5qgvCAfRSocIoKgmMAhA=",
|
||||||
|
"owner": "jkachmar",
|
||||||
|
"repo": "easy-hls-nix",
|
||||||
|
"rev": "7c123399ef8a67dc0e505d9cf7f2c7f64f1cd847",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "jkachmar",
|
||||||
|
"repo": "easy-hls-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1637014545,
|
||||||
|
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1637841632,
|
||||||
|
"narHash": "sha256-QYqiKHdda0EOnLGQCHE+GluD/Lq2EJj4hVTooPM55Ic=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "73369f8d0864854d1acfa7f1e6217f7d6b6e3fa1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"easy-hls": "easy-hls",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
51
flake.nix
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
description = "rauhala.info";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
easy-hls = {
|
||||||
|
url = "github:jkachmar/easy-hls-nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils, easy-hls }:
|
||||||
|
{
|
||||||
|
overlay = final: prev: {
|
||||||
|
build-rauhala-info = final.haskellPackages.build-rauhala-info;
|
||||||
|
rauhala-info = final.callPackage ./rauhala.info { site = final.build-rauhala-info; };
|
||||||
|
haskellPackages = prev.haskellPackages.override ( old: {
|
||||||
|
overrides = final.lib.composeExtensions ( old.overrides or (_: _: {})) (f: p: {
|
||||||
|
build-rauhala-info = f.callPackage ./site {};
|
||||||
|
});
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; };
|
||||||
|
hp = pkgs.haskellPackages;
|
||||||
|
hls = easy-hls.defaultPackage.${system};
|
||||||
|
in
|
||||||
|
rec {
|
||||||
|
packages = { inherit (pkgs) build-rauhala-info rauhala-info; };
|
||||||
|
applications.build-rauhala-info = flake-utils.lib.mkApp {
|
||||||
|
drv = packages.build-rauhala-info;
|
||||||
|
exePath = "/bin/site";
|
||||||
|
};
|
||||||
|
defaultPackage = packages.rauhala-info;
|
||||||
|
defaultApp = applications.build-rauhala-info;
|
||||||
|
devShell = hp.shellFor {
|
||||||
|
packages = h: [h.build-rauhala-info];
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
ghcid
|
||||||
|
cabal-install
|
||||||
|
stylish-haskell
|
||||||
|
entr
|
||||||
|
hls
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
Before Width: | Height: | Size: 10 KiB |
22
js/api.js
@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
var getApiIpfsCurrent = function(onSuccess, onError) {
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('GET', '/api/ipfs/current', true);
|
|
||||||
xhr.setRequestHeader('Accept', 'application/json');
|
|
||||||
xhr.onreadystatechange = function () {
|
|
||||||
var res = null;
|
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
if (xhr.status === 204 || xhr.status === 205) {
|
|
||||||
onSuccess();
|
|
||||||
} else if (xhr.status >= 200 && xhr.status < 300) {
|
|
||||||
try { res = JSON.parse(xhr.responseText); } catch (e) { onError(e); }
|
|
||||||
if (res) onSuccess(res);
|
|
||||||
} else {
|
|
||||||
try { res = JSON.parse(xhr.responseText); } catch (e) { onError(e); }
|
|
||||||
if (res) onError(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send(null);
|
|
||||||
};
|
|
||||||
|
|
10
js/app.js
@ -1,10 +0,0 @@
|
|||||||
$(document).ready(function() {
|
|
||||||
var success = function(x) {
|
|
||||||
$("#ipfs > em").html(x);
|
|
||||||
};
|
|
||||||
var error = function(x) {
|
|
||||||
$("#ipfs").hide();
|
|
||||||
console.log("ipfs hash not found: " + x);
|
|
||||||
};
|
|
||||||
getApiIpfsCurrent(success, error);
|
|
||||||
});
|
|
2
js/jquery-3.3.1.min.js
vendored
7
nixpkgs.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"url": "https://github.com/NixOS/nixpkgs",
|
||||||
|
"rev": "7e1f60dfbba67b975d1a77d710a6f1437fd9709c",
|
||||||
|
"date": "2020-02-01T09:20:37-05:00",
|
||||||
|
"sha256": "0vk55459iljr5dzwnr5661l44b0wdc15952lk2rjcmxr1620yr5v",
|
||||||
|
"fetchSubmodules": false
|
||||||
|
}
|
@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
title: Extensible configuration Pt. 1
|
|
||||||
date: 2019-03-27
|
|
||||||
---
|
|
||||||
|
|
||||||
This is the first part of a series where I'm going through how to make
|
|
||||||
extensible configuration. There is nothing groundbreaking or new in this
|
|
||||||
series, it's just me going through different implementations and trying to
|
|
||||||
understand them.
|
|
||||||
|
|
||||||
The source material for this post is [the fixed point implementation](https://github.com/NixOS/nixpkgs/blob/master/lib/fixed-points.nix) for nix.
|
|
||||||
|
|
||||||
By extensible configuration, I'm talking about nix style extensible
|
|
||||||
configuration, like overlays, overrides and extensions. Let's see an example of
|
|
||||||
an extensible configuration.
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
{ haskellPackages, fetchFromGitHub }:
|
|
||||||
|
|
||||||
let
|
|
||||||
purescript = fetchFromGitHub {
|
|
||||||
owner = "purescript";
|
|
||||||
repo = "purescript";
|
|
||||||
rev = "2cb4a6496052db726e099539be682b87585af494";
|
|
||||||
sha256 = "1v4gs08xnqgym6jj3drkzbic7ln3hfmflpbpij3qzwxsmqd2abr7";
|
|
||||||
}
|
|
||||||
hp = haskellPackages.extend (self: super: {
|
|
||||||
purescript = super.callCabal2nix "purescript" purescript {};
|
|
||||||
});
|
|
||||||
|
|
||||||
in
|
|
||||||
|
|
||||||
hp.purescript;
|
|
||||||
```
|
|
||||||
|
|
||||||
On a high level we are augmenting the `haskellPackages` attrset by replacing
|
|
||||||
the existing purescript package with a different one. The extension is a
|
|
||||||
function that takes two arguments, `self` and `super`. `super` is the original
|
|
||||||
non-lazy value and `self` is the lazy value that corresponds to the value at
|
|
||||||
end.
|
|
||||||
|
|
||||||
The first step on this journey is done by getting to know `fix`. Fix is
|
|
||||||
described being the least fixed point of a function. In practice it's a
|
|
||||||
function allowing declaring recursive functions without explicit recursion.
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
fix = f: let x = f x; in x
|
|
||||||
```
|
|
||||||
|
|
||||||
With fix you can have access to the lazy `self` value. It's value is whatever
|
|
||||||
would have been computed in the end. As it is lazy, it is possible to end up in
|
|
||||||
a recursive loop if there is a cyclic dependency.
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
let recursive = fix (self: {
|
|
||||||
foo = 3;
|
|
||||||
bar = self.foo + 1;
|
|
||||||
});
|
|
||||||
infinite = fix (self: {
|
|
||||||
foo = self.bar + 1;
|
|
||||||
bar = self.foo + 1;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
You can try those yourself. The first version is fine and returns an attrset
|
|
||||||
like you would expect. The second one has a cyclic dependency and nix helpfully
|
|
||||||
errors out.
|
|
||||||
|
|
||||||
The next step is making a function that has access to the unmodified original
|
|
||||||
values. This is managed through the `extends` function. It took a while for me to understand what's happening in there, but luckily nix has [good documentation](https://github.com/NixOS/nixpkgs/blob/67b1265fb3d38ead5a57fee838405a2d997777c2/lib/fixed-points.nix#L37-L65) for it.
|
|
||||||
|
|
||||||
``` nix
|
|
||||||
extends = f: rattrs: self: let super = rattrs self; in super // f self super
|
|
||||||
```
|
|
||||||
|
|
||||||
- https://elvishjerricco.github.io/2017/04/01/nix-style-configs-in-haskell.html
|
|
||||||
- https://github.com/NixOS/nixpkgs/blob/master/lib/fixed-points.nix
|
|
||||||
- https://chshersh.github.io/posts/2019-03-25-comonadic-builders
|
|
148
posts/demobot.md
@ -1,148 +0,0 @@
|
|||||||
---
|
|
||||||
title: Functional architecture Pt. 1
|
|
||||||
date: 2018-12-25
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
I'm lucky enough to work with Haskell professionally which gives me some view
|
|
||||||
to good and maintainable real world architecture. In my opinion, one of the
|
|
||||||
biggest contributing factors to how your general architecture is defined, is
|
|
||||||
determined by the base application monad stack you are using.
|
|
||||||
|
|
||||||
Our actual product is mostly in the regular `LoggingT (ReaderT app IO)` base
|
|
||||||
monad with whatever style you would imagine with that base monad in place. It's
|
|
||||||
not entirely consistent, but close enough.
|
|
||||||
|
|
||||||
With all the talk about just having `IO`, `ReaderT app IO`, free monads or
|
|
||||||
tagless final monads, I thought of trying different styles. For this post I'm
|
|
||||||
focusing on the tagless final since it's most interesting for me right now.
|
|
||||||
|
|
||||||
`IO`
|
|
||||||
|
|
||||||
: The most basic style. This is pretty much only suitable for the most basic
|
|
||||||
of needs.
|
|
||||||
|
|
||||||
`ReaderT app IO`
|
|
||||||
|
|
||||||
: How we mostly define the base monad. This is a really good way of doing
|
|
||||||
things, it gives you a lot of leeway on how you can define the rest of your
|
|
||||||
application.
|
|
||||||
|
|
||||||
`Free monads`
|
|
||||||
|
|
||||||
: Free monads are a way of having a small constrained DSL or monad stack for
|
|
||||||
defining your application. By constraining the user, you are also reducing the
|
|
||||||
area for bugs. There is also some possibility for introspection, but usually
|
|
||||||
this isn't a usable feature. Also since free monad applications need the full
|
|
||||||
AST, they're quite a bit slower than the other solutions.
|
|
||||||
|
|
||||||
`Tagless final`
|
|
||||||
|
|
||||||
: This is something I'm the least familiar with. If I have understood
|
|
||||||
correctly, free monads and tagless final are more or less equivalent solutions
|
|
||||||
in their power, but in tagless final you aren't creating the AST anywhere,
|
|
||||||
which also means that you aren't paying for it either.
|
|
||||||
|
|
||||||
That out of the way, I had a small project idea for a bot that's easy to
|
|
||||||
contribute to, difficult to make errors and easy to reason about. The project
|
|
||||||
is at most a proof-of-concept and most definitely not production quality.
|
|
||||||
Still, I hope it's complex enough to showcase the architecture.
|
|
||||||
|
|
||||||
The full source code is available [at my git repository](https://git.rauhala.info/MasseR/demobot).
|
|
||||||
|
|
||||||
For the architecture to make sense, let me introduce two different actors: a
|
|
||||||
*core contributor* that's familiar with Haskell and a *external contributor*
|
|
||||||
that's familiar with programming, not necessarily with Haskell.
|
|
||||||
|
|
||||||
The repository is split into two parts, the library and the application.
|
|
||||||
|
|
||||||
The library
|
|
||||||
|
|
||||||
: Provides the restricted monad classes (tagless final), extension points and
|
|
||||||
the core bot main loop.
|
|
||||||
|
|
||||||
The application
|
|
||||||
|
|
||||||
: Provides the implementation for the tagless final type classes, meaning
|
|
||||||
that the application defines how the networking stack is handled, how database
|
|
||||||
connectivity is done and so on. It also collects all the extensions for that
|
|
||||||
specific application.
|
|
||||||
|
|
||||||
The *core contributor* is responsible for maintaining the library as well as
|
|
||||||
the type class instances for the application type. The *external contributor*
|
|
||||||
is responsible for maintaining one or multiple extensions that are restricted
|
|
||||||
in their capability and complexity.
|
|
||||||
|
|
||||||
I'm restricting the capabilities of the monad in the library and extensions,
|
|
||||||
meaning that I'm not allowing any IO. For example the networking is handled by
|
|
||||||
a single `MonadNetwork` type class. This is the most complex type class in the
|
|
||||||
library right now, using type families for defining a specific extension point
|
|
||||||
for the messages. This could be something like 'event type' for Flowdock
|
|
||||||
messages or 'source channel' for IRC messages.
|
|
||||||
|
|
||||||
~~~haskell
|
|
||||||
data Request meta = Request { content :: Text
|
|
||||||
, meta :: meta }
|
|
||||||
data Response meta = Response { content :: Text
|
|
||||||
, meta :: meta }
|
|
||||||
|
|
||||||
class Monad m => MonadNetwork m where
|
|
||||||
type Meta m :: *
|
|
||||||
recvMsg :: m (Request (Meta m))
|
|
||||||
putMsg :: Response (Meta m) -> m ()
|
|
||||||
~~~
|
|
||||||
|
|
||||||
Then we have the extension point which is more or less just a `Request -> m (Maybe Response)`. I'm using rank n types here for qualifying the `Meta`
|
|
||||||
extension point and forcing the allowed type classes to be a subset of the
|
|
||||||
application monad stack, I don't want extension writers to be able to write
|
|
||||||
messages to the bot network by themselves.
|
|
||||||
|
|
||||||
~~~haskell
|
|
||||||
data Extension meta =
|
|
||||||
Extension { act :: forall m. (meta ~ Meta m, MonadExtension m) => Request meta -> m (Maybe (Response meta))
|
|
||||||
, name :: String }
|
|
||||||
~~~
|
|
||||||
|
|
||||||
Last part of the library is the main loop, which is basically a free monad
|
|
||||||
(tagless final) waiting for an interpreter. At least in this POC I find this
|
|
||||||
style to be really good, it's really simplified, easy to read and hides a lot
|
|
||||||
of the complexity, while bringing forth the core algorithm.
|
|
||||||
|
|
||||||
~~~haskell
|
|
||||||
mainLoop :: forall m. (MonadCatch m, MonadBot m) => [Extension (Meta m)] -> m ()
|
|
||||||
mainLoop extensions = forever $ catch go handleFail
|
|
||||||
where
|
|
||||||
handleFail :: SomeException -> m ()
|
|
||||||
handleFail e = logError $ tshow e
|
|
||||||
go :: m ()
|
|
||||||
go = do
|
|
||||||
msg <- recvMsg
|
|
||||||
responses <- catMaybes <$> mapM (`act` msg) extensions
|
|
||||||
mapM_ putMsg responses
|
|
||||||
~~~
|
|
||||||
|
|
||||||
Then comes the actual application where we write the effectful interpreters. In
|
|
||||||
this POC the interpreter is just a `LoggingT IO a` with the semantics of
|
|
||||||
stdin/stdout. This is the only file where we're actually interacting with the
|
|
||||||
outside world, everything else is just pure code.
|
|
||||||
|
|
||||||
~~~haskell
|
|
||||||
instance MonadNetwork AppM where
|
|
||||||
type Meta AppM = ()
|
|
||||||
recvMsg = Request <$> liftIO T.getLine <*> pure ()
|
|
||||||
putMsg Response{..} = liftIO . T.putStrLn $ content
|
|
||||||
~~~
|
|
||||||
|
|
||||||
Writing the extensions was the responsibility of *external contributors* and we
|
|
||||||
already saw how the actual extension point was defined above. Using these
|
|
||||||
extension points is really simple and here we see how the implementation is
|
|
||||||
just a simple `Request -> m (Maybe Response)`.
|
|
||||||
|
|
||||||
~~~haskell
|
|
||||||
extension :: Extension ()
|
|
||||||
extension = Extension{..}
|
|
||||||
where
|
|
||||||
name = "hello world"
|
|
||||||
act Request{..} | "hello" `T.isPrefixOf` content = return $ Just $ Response "Hello to you" ()
|
|
||||||
| otherwise = return Nothing
|
|
||||||
~~~
|
|
2
rauhala.info/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
_site
|
||||||
|
_cache
|
8
rauhala.info/contact.markdown
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: Contact
|
||||||
|
---
|
||||||
|
|
||||||
|
I live in Espoo Finland. You can contact me on any of the following services.
|
||||||
|
|
||||||
|
- **Email**: mats.rauhala@iki.fi
|
||||||
|
- **Mastodon**: MasseR@haskell.social
|
@ -39,6 +39,18 @@ h2 {
|
|||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.projectlist {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.blog {
|
||||||
|
/* display: flex; */
|
||||||
|
/* align-items: center; */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
article .header {
|
article .header {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@ -51,6 +63,7 @@ article .header {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 319px) {
|
@media (max-width: 319px) {
|
||||||
body {
|
body {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
@ -107,14 +120,40 @@ article .header {
|
|||||||
display: inline;
|
display: inline;
|
||||||
margin: 0 0.6rem;
|
margin: 0 0.6rem;
|
||||||
}
|
}
|
||||||
|
div.sidebar-container {
|
||||||
|
/* display: grid; */
|
||||||
|
/* grid-template-columns: minmax(60%, 85%) 1fr; */
|
||||||
|
}
|
||||||
|
div.sidebar-container > div.sidebar {
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 0.2rem solid #000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
body {
|
body {
|
||||||
width: 85rem;
|
width: 100rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
div.sidebar-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(60%, 85%) 1fr;
|
||||||
|
}
|
||||||
|
div.sidebar-container > div.sidebar {
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-left: 2rem;
|
||||||
|
border-left: 0.2rem solid #000;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
width: 60rem;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
header {
|
header {
|
||||||
margin: 0 0 3rem;
|
margin: 0 0 3rem;
|
||||||
padding: 1.2rem 0;
|
padding: 1.2rem 0;
|
@ -1,18 +1,6 @@
|
|||||||
/* Generated by pandoc. */
|
/* Generated by pandoc. */
|
||||||
table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre.sourceCode
|
table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre
|
||||||
{
|
{ margin: 0; padding: 0; border: 0; vertical-align: baseline; border: none; }
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
/* border: 0; */
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
pre.sourceCode
|
|
||||||
{
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: rgb(238,238,255);
|
|
||||||
padding: 10px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
td.lineNumbers { border-right: 1px solid #AAAAAA; text-align: right; color: #AAAAAA; padding-right: 5px; padding-left: 5px; }
|
td.lineNumbers { border-right: 1px solid #AAAAAA; text-align: right; color: #AAAAAA; padding-right: 5px; padding-left: 5px; }
|
||||||
td.sourceCode { padding-left: 5px; }
|
td.sourceCode { padding-left: 5px; }
|
||||||
.sourceCode span.kw { color: #007020; font-weight: bold; }
|
.sourceCode span.kw { color: #007020; font-weight: bold; }
|
24
rauhala.info/default.nix
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{ stdenv, glibcLocales, site }:
|
||||||
|
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
pname = "rauhala.info";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
src = builtins.filterSource (path: type: baseNameOf path != "_cache" && baseNameOf path != "_site") ./.;
|
||||||
|
|
||||||
|
phases = ["buildPhase" "installPhase"];
|
||||||
|
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
export LOCALE_ARCHIVE="${glibcLocales}/lib/locale/locale-archive"
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
|
||||||
|
cp -r $src/* .
|
||||||
|
${site}/bin/site build
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/share/
|
||||||
|
cp -r _site/* $out/share/
|
||||||
|
'';
|
||||||
|
}
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 684 B |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 485 B After Width: | Height: | Size: 485 B |
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 684 B |
BIN
rauhala.info/images/profile.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
@ -15,12 +15,15 @@ My [GPG key](./resources/2104943D6033C.txt)
|
|||||||
|
|
||||||
### Notable experience and interests
|
### Notable experience and interests
|
||||||
|
|
||||||
- **Haskell**
|
- **Relex Oy**
|
||||||
- Working on an internal Haskell based tool at Relex Oy
|
- Working as a Senior Software Developer at Relex Oy
|
||||||
|
- Three distinct products, all of which written in Haskell
|
||||||
|
- Internal integration tool
|
||||||
|
- Relex Deploy 1, a tool for deploying the core product to hosts
|
||||||
|
- Relex Deploy 2, a tool for deploying the new and shiny core product to hosts
|
||||||
|
- **Open source**
|
||||||
- Some contributions to [darcshub](https://hub.darcs.net/)
|
- Some contributions to [darcshub](https://hub.darcs.net/)
|
||||||
- Some contributions to [xmonad](https://github.com/xmonad)
|
- Some contributions to [xmonad](https://github.com/xmonad)
|
||||||
- Dozens of personal projects, most of which ended up being just tests for
|
|
||||||
different libraries, techniques or algorithms.
|
|
||||||
- **Systems integrations**
|
- **Systems integrations**
|
||||||
- Working as a consultant from Avoltus Oy to different companies using
|
- Working as a consultant from Avoltus Oy to different companies using
|
||||||
[Mulesoft](https://developer.mulesoft.com/). Systems include webshops,
|
[Mulesoft](https://developer.mulesoft.com/). Systems include webshops,
|
||||||
@ -28,4 +31,4 @@ My [GPG key](./resources/2104943D6033C.txt)
|
|||||||
- **Java**
|
- **Java**
|
||||||
- Bunch of different smaller projects while working at Avoltus Oy.
|
- Bunch of different smaller projects while working at Avoltus Oy.
|
||||||
- **Other**
|
- **Other**
|
||||||
- I was involved in creating an email solicit platform.
|
- I was involved in creating an email advertising platform.
|
254
rauhala.info/posts/2021-01-26-Tests-with-Deriving.md
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
---
|
||||||
|
title: Tests with Deriving Via
|
||||||
|
tags: haskell, testing, pbt
|
||||||
|
---
|
||||||
|
|
||||||
|
I have been using both `hedgehog` and `QuickCheck` based property-based testing
|
||||||
|
frameworks, I'm fairly comfortable in writing tests and generators in both.
|
||||||
|
Theoretical aspects aside, for a user, I feel like `hedgehog` is more
|
||||||
|
ergonomic as it does automatic shrinking *and* does away with typeclasses. The
|
||||||
|
former is important as writing good shrinkers is hard, remembering to write
|
||||||
|
shrinkers is even harder. The latter is important when you need to modify your
|
||||||
|
generation for some tests.
|
||||||
|
|
||||||
|
In this post, I'll show that using `DerivingVia` extension and generic
|
||||||
|
coercions can help you write almost as ergonomic `Arbitrary` definitions for
|
||||||
|
`QuickCheck`. The initial idea is taken from the
|
||||||
|
[Deriving Via](https://www.kosmikus.org/DerivingVia/deriving-via-paper.pdf) paper,
|
||||||
|
but taken a little bit further. This post assumes some level of understanding
|
||||||
|
of type level programming.
|
||||||
|
|
||||||
|
For the examples, we're using a `Person` as shown in the examples below. The
|
||||||
|
test we'll implement will be the `tripping` property. For the *expected*
|
||||||
|
values, the `name` is something name-like and `age` is a range between 1-99.
|
||||||
|
|
||||||
|
I'll use `hedgehog` to write the ideal case. The generator is light-weight, but
|
||||||
|
has been customized for the business case. I'm using the `hedgehog-corpus`
|
||||||
|
package for the name-like generation.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
import GHC.Generics (Generic)
|
||||||
|
|
||||||
|
import Data.Text (Text)
|
||||||
|
|
||||||
|
import qualified Data.Aeson as A
|
||||||
|
|
||||||
|
import Hedgehog
|
||||||
|
import qualified Hedgehog.Gen as Gen
|
||||||
|
import qualified Hedgehog.Range as Range
|
||||||
|
import qualified Hedgehog.Corpus as Corpus
|
||||||
|
|
||||||
|
data Person
|
||||||
|
= Person { name :: Text
|
||||||
|
, age :: Int
|
||||||
|
}
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (A.ToJSON, A.FromJSON)
|
||||||
|
|
||||||
|
genValidPerson :: Gen Person
|
||||||
|
genValidPerson =
|
||||||
|
Person <$> Gen.element Corpus.simpsons
|
||||||
|
<*> Gen.integral (Range.linear 0 99)
|
||||||
|
|
||||||
|
prop_encoding :: Property
|
||||||
|
prop_encoding = property $ do
|
||||||
|
p <- forAll genValidPerson
|
||||||
|
pure p === A.eitherDecode (A.encode p)
|
||||||
|
```
|
||||||
|
|
||||||
|
For comparison, this is what I would write with QuickCheck without any helpers.
|
||||||
|
There's quite a bit of added complexity, especially in the shrinker, and only
|
||||||
|
with two fields.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
import GHC.Generics (Generic)
|
||||||
|
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Aeson as A
|
||||||
|
|
||||||
|
import Test.QuickCheck
|
||||||
|
|
||||||
|
data Person
|
||||||
|
= Person { name :: Text
|
||||||
|
, age :: Int
|
||||||
|
}
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (A.ToJSON, A.FromJSON)
|
||||||
|
|
||||||
|
instance Arbitrary Person where
|
||||||
|
arbitrary = Person <$> elements simpsons <*> choose (1,99)
|
||||||
|
where
|
||||||
|
simpsons = ["bart", "marge", "homer", "lisa", "ned"]
|
||||||
|
shrink Person{name,age} =
|
||||||
|
[Person name' age'
|
||||||
|
| name' <- [name]
|
||||||
|
, age' <- shrinkIntegral age
|
||||||
|
, age' >= 1
|
||||||
|
, age' <= 99
|
||||||
|
]
|
||||||
|
|
||||||
|
prop_encoding :: Person -> Property
|
||||||
|
prop_encoding p = pure p === A.eitherDecode (A.encode p)
|
||||||
|
```
|
||||||
|
|
||||||
|
Good, now that the base is done, let's see what we can do about making
|
||||||
|
`QuickCheck` more ergonomic. The solution I'm outlining here relies on these
|
||||||
|
features.
|
||||||
|
|
||||||
|
- `DerivingVia` extension which can automatically generate instances for you if two types are `Coercible`
|
||||||
|
- Isomorphism between the `Generic` representation of two types. For example `(a,b)` has a `Generic` representation that is the same as `data Foo = Foo a b`
|
||||||
|
- `QuickCheck` modifiers, for example `PrintableString` which modify the arbitrary generation
|
||||||
|
|
||||||
|
The paper defines this piece of code for deriving `Arbitrary` instances for
|
||||||
|
anything that is generically isomorphic to something that is already an
|
||||||
|
instance.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
newtype SameRepAs a b = SameRepAs a
|
||||||
|
|
||||||
|
instance
|
||||||
|
( Generic a
|
||||||
|
, Generic b
|
||||||
|
, Arbitrary b
|
||||||
|
, Coercible (Rep a ()) (Rep b ())
|
||||||
|
)
|
||||||
|
=> Arbitrary (a `SameRepAs` b) where
|
||||||
|
arbitrary = SameRepAs . coerceViaRep <$> arbitrary
|
||||||
|
where
|
||||||
|
coerceViaRep :: b -> a
|
||||||
|
coerceViaRep =
|
||||||
|
to . (coerce :: Rep b () -> Rep a ()) . from
|
||||||
|
```
|
||||||
|
|
||||||
|
For my implementation, I'll be cleaning the code from the paper. I'm swapping
|
||||||
|
the type parameters of the newtype and extract the coercion function to
|
||||||
|
top-level so that I can define the `shrink` as well.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
newtype Isomorphic a b = Isomorphic b
|
||||||
|
|
||||||
|
type GenericCoercible a b =
|
||||||
|
( Generic a
|
||||||
|
, Generic b
|
||||||
|
, Coercible (Rep a ()) (Rep b ())
|
||||||
|
)
|
||||||
|
|
||||||
|
genericCoerce :: forall a b. GenericCoercible a b => a -> b
|
||||||
|
genericCoerce =
|
||||||
|
to . (coerce @(Rep a ()) @(Rep b ())) . from
|
||||||
|
|
||||||
|
instance
|
||||||
|
( Arbitrary a
|
||||||
|
, GenericCoercible a b
|
||||||
|
)
|
||||||
|
=> Arbitrary (a `Isomorphic` b) where
|
||||||
|
arbitrary = Isomorphic . genericCoerce @a @b <$> arbitrary
|
||||||
|
shrink (Isomorphic b) =
|
||||||
|
Isomorphic . genericCoerce @a @b
|
||||||
|
<$> shrink (genericCoerce @b @a b)
|
||||||
|
```
|
||||||
|
|
||||||
|
With this, we can now write `Arbitrary` instances using the tuple
|
||||||
|
representation as an intermediary. At least as long as the child types have
|
||||||
|
their instances properly set.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
data Person
|
||||||
|
= Person { name :: Text
|
||||||
|
, age :: Int
|
||||||
|
}
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (A.ToJSON, A.FromJSON)
|
||||||
|
deriving (Arbitrary) via ((Text, Int) `Isomorphic` Person)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is already a marked improvement to the original `Arbitrary` instance we
|
||||||
|
wrote, but this does not yet satisfy our original requirement of generating
|
||||||
|
only 'valid' persons. I would like to modify the instance generation on a more
|
||||||
|
ad-hoc fashion. For this to happen, I would need some *modifiers* that control
|
||||||
|
the arbitrary generation. I would like to write something like the instance
|
||||||
|
below.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
type Simpsons = '["marge", "bart", "homer", "lisa", "ned"]
|
||||||
|
data Person
|
||||||
|
= Person { name :: Text
|
||||||
|
, age :: Int
|
||||||
|
}
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (A.ToJSON, A.FromJSON)
|
||||||
|
deriving (Arbitrary)
|
||||||
|
via ((Corpus Simpsons Text, Range 1 99 Int) `Isomorphic` Person)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Let's start by defining the `Range` as it's more straightforward. This is just
|
||||||
|
a `newtype` with a couple of phantom type variables, which is used in choosing
|
||||||
|
the range of the generator. Shrinking is already quite complex (and probably
|
||||||
|
not optimal!), I wouldn't want to write this multiple times.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
newtype Range (from :: Nat) (to :: Nat) a = Range a
|
||||||
|
|
||||||
|
instance
|
||||||
|
( KnownNat from
|
||||||
|
, KnownNat to
|
||||||
|
, Num a
|
||||||
|
, Ord a
|
||||||
|
, Integral a
|
||||||
|
) => Arbitrary (Range from to a) where
|
||||||
|
arbitrary = Range . fromInteger <$> choose (natVal $ Proxy @from, natVal $ Proxy @to)
|
||||||
|
shrink (Range x) = Range <$> shrunk
|
||||||
|
where
|
||||||
|
shrunk =
|
||||||
|
[ x'
|
||||||
|
| x' <- shrinkIntegral x
|
||||||
|
, x >= fromInteger (natVal $ Proxy @from)
|
||||||
|
, x <= fromInteger (natVal $ Proxy @to)
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the corpus. Just like the `Range` it's a `newtype` with a phantom
|
||||||
|
variable, providing the input for the random generation. There's an extra
|
||||||
|
typeclass involved to act as a typelevel function.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
newtype Corpus (corpus :: [Symbol]) a = Corpus a
|
||||||
|
|
||||||
|
class FromCorpus (corpus :: [Symbol]) where
|
||||||
|
fromCorpus :: [String]
|
||||||
|
|
||||||
|
instance FromCorpus '[] where
|
||||||
|
fromCorpus = []
|
||||||
|
|
||||||
|
instance (KnownSymbol x, FromCorpus xs) => FromCorpus (x ': xs) where
|
||||||
|
fromCorpus = symbolVal (Proxy @x) : fromCorpus @xs
|
||||||
|
|
||||||
|
instance (FromCorpus corpus, IsString x) => Arbitrary (Corpus corpus x) where
|
||||||
|
arbitrary = Corpus . fromString <$> elements (fromCorpus @corpus)
|
||||||
|
```
|
||||||
|
|
||||||
|
With these instances out of the way, we can redo our original test with
|
||||||
|
automatic instances.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
import GHC.Generics (Generic)
|
||||||
|
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Aeson as A
|
||||||
|
|
||||||
|
import Test.QuickCheck
|
||||||
|
|
||||||
|
import Isomorphic
|
||||||
|
|
||||||
|
data Person
|
||||||
|
= Person { name :: Text
|
||||||
|
, age :: Int
|
||||||
|
}
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (A.ToJSON, A.FromJSON)
|
||||||
|
deriving Arbitrary via ((Corpus Simpsons Text, Range 1 99 Int) `Isomorphic` Person)
|
||||||
|
|
||||||
|
prop_encoding :: Person -> Property
|
||||||
|
prop_encoding p = pure p === A.eitherDecode (A.encode p)
|
||||||
|
```
|
37
rauhala.info/projects/2021-01-21-bidirectional.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: bidirectional
|
||||||
|
github: https://github.com/MasseR/bidirectional
|
||||||
|
issues: https://github.com/MasseR/bidirectional/issues
|
||||||
|
badge: https://github.com/MasseR/bidirectional/workflows/Test/badge.svg
|
||||||
|
---
|
||||||
|
|
||||||
|
Bidirectional serialization based on Lysxia's post on [Monadic profunctors for bidirectional programming](https://blog.poisson.chat/posts/2017-01-01-monadic-profunctors.html).
|
||||||
|
|
||||||
|
|
||||||
|
Let's assume we have a parser like the following
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
int :: Parser (ReaderT String Maybe) (Writer [Int]) Int Int
|
||||||
|
int = parser (ReaderT readMaybe) (\x -> x <$ tell [show x])
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can use the parser for parsing:
|
||||||
|
|
||||||
|
```
|
||||||
|
> runReaderT (decode int) "3"
|
||||||
|
Just 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for encoding:
|
||||||
|
|
||||||
|
```
|
||||||
|
> execWriter (encode int 3)
|
||||||
|
["3"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Or combine both of them
|
||||||
|
|
||||||
|
```
|
||||||
|
> runReaderT (decode int) $ head $ execWriter $ encode int 3
|
||||||
|
Just 3
|
||||||
|
```
|
23
rauhala.info/projects/2021-01-21-phrase.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: phrase
|
||||||
|
github: https://github.com/MasseR/phrase
|
||||||
|
issues: https://github.com/MasseR/phrase/issues
|
||||||
|
badge: https://github.com/MasseR/phrase/workflows/Run nix build/badge.svg
|
||||||
|
---
|
||||||
|
|
||||||
|
This project is a command line tool for generating password phrases using the
|
||||||
|
[diceware method](https://diceware.dmuth.org/). The passwords are stored in a
|
||||||
|
folder structure that is compatible with the
|
||||||
|
[pass](https://www.passwordstore.org/) password store manager.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
$ phrase asd
|
||||||
|
inlet area crux
|
||||||
|
|
||||||
|
$ pass show asd
|
||||||
|
inlet area crux
|
||||||
|
|
||||||
|
$ file ~/.password-store/asd.gpg
|
||||||
|
```
|
17
rauhala.info/projects/2021-01-21-zettelkast.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: zettelkast
|
||||||
|
github: https://github.com/MasseR/zettelkast
|
||||||
|
issues: https://github.com/MasseR/zettelkast/issues
|
||||||
|
badge: https://github.com/MasseR/zettelkast/workflows/Test/badge.svg
|
||||||
|
---
|
||||||
|
|
||||||
|
Command-line tool for managing zettelkast documents. The tool primarily focuses
|
||||||
|
on providing unique ids and showing a graph of document connections. It tries
|
||||||
|
to be as unintrusive as possible.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ zettelkast help
|
||||||
|
$ zettelkast new
|
||||||
|
$ zettelkast list
|
||||||
|
$ zettelkast open <id>
|
||||||
|
```
|
@ -10,30 +10,30 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">
|
<!-- <div class="logo"> -->
|
||||||
<a href="/">rauhala.info</a>
|
<!-- <a href="/">rauhala.info</a> -->
|
||||||
</div>
|
<!-- </div> -->
|
||||||
<nav>
|
<nav>
|
||||||
<!-- Git logo from https://git-scm.com/downloads/logos -->
|
<!-- Git logo from https://git-scm.com/downloads/logos -->
|
||||||
<!-- Logo by Jason Long -->
|
<!-- Logo by Jason Long -->
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<a href="/posts.html">Posts</a>
|
||||||
|
<a href="/projects.html">Projects</a>
|
||||||
<a href="https://git.rauhala.info"><img src="/images/git_16.png" alt="git" /></a>
|
<a href="https://git.rauhala.info"><img src="/images/git_16.png" alt="git" /></a>
|
||||||
<a href="/contact.html">Contact</a>
|
<a href="/contact.html">Contact</a>
|
||||||
<a href="/posts.html">Posts</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main role="main">
|
<main role="main">
|
||||||
|
<div>
|
||||||
<h1>$title$</h1>
|
<h1>$title$</h1>
|
||||||
$body$
|
$body$
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
Site proudly generated by
|
Site proudly generated by
|
||||||
<a href="http://jaspervdj.be/hakyll">Hakyll</a>
|
<a href="http://jaspervdj.be/hakyll">Hakyll</a>
|
||||||
<span id="ipfs">and found on IPFS as <em></em></ipfs>
|
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
<script type="application/javascript" src="/js/jquery-3.3.1.min.js"></script>
|
|
||||||
<script type="application/javascript" src="/js/api.js"></script>
|
|
||||||
<script type="application/javascript" src="/js/app.js"></script>
|
|
||||||
</html>
|
</html>
|
7
rauhala.info/templates/post-list.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<ul>
|
||||||
|
$for(items)$
|
||||||
|
<li>
|
||||||
|
<a href="$url$">$title$</a> - $date$
|
||||||
|
</li>
|
||||||
|
$endfor$
|
||||||
|
</ul>
|
8
rauhala.info/templates/post.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<article>
|
||||||
|
<section class="header">
|
||||||
|
Posted on $date$
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
$body$
|
||||||
|
</section>
|
||||||
|
</article>
|
11
rauhala.info/templates/project-list.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<ul>
|
||||||
|
$for(items)$
|
||||||
|
<li>
|
||||||
|
<div class="projectlist">
|
||||||
|
<a href="$url$">$title$</a>
|
||||||
|
<a href="$github$">$if(badge)$<img src="$badge$" />$else$ Github $endif$</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
$endfor$
|
||||||
|
</ul>
|
||||||
|
|
12
rauhala.info/templates/project.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<div class="sidebar-container">
|
||||||
|
<article>
|
||||||
|
<section>
|
||||||
|
$body$
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<div class="sidebar">
|
||||||
|
<a href="$github$">Github</a>
|
||||||
|
<a href="$issues$">Issues</a>
|
||||||
|
$if(badge)$<img src="$badge$" />$endif$
|
||||||
|
</div>
|
||||||
|
</div>
|
111
rauhala.info/well-known/keybase.txt
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
==================================================================
|
||||||
|
https://keybase.io/masser
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
I hereby claim:
|
||||||
|
|
||||||
|
* I am an admin of https://masser.keybase.pub
|
||||||
|
* I am masser (https://keybase.io/masser) on keybase.
|
||||||
|
* I have a public key ASD7WFMicMJhoArgnvPuR7Dc92WStIUWWXcyngnsTE_IaQo
|
||||||
|
|
||||||
|
To do so, I am signing this object:
|
||||||
|
|
||||||
|
{
|
||||||
|
"body": {
|
||||||
|
"key": {
|
||||||
|
"eldest_kid": "01202eb5d3b8d2fc63e4bc9c6edefb3a38f1043dfdc44a7268393951fe0ea5214cf00a",
|
||||||
|
"host": "keybase.io",
|
||||||
|
"kid": "0120fb58532270c261a00ae09ef3ee47b0dcf76592b485165977329e09ec4c4fc8690a",
|
||||||
|
"uid": "b01557931e13482e9e646593a64e9119",
|
||||||
|
"username": "masser"
|
||||||
|
},
|
||||||
|
"merkle_root": {
|
||||||
|
"ctime": 1580587604,
|
||||||
|
"hash": "890506579950548b2aa6fd0f35850b9fdb1d064786714f18515d0e3089cdc2458c5fe847d40ca71c2e021ca11677de055ac97804c53d1bef89432f88b57599d4",
|
||||||
|
"hash_meta": "104419595abc52918530f85495417faf35c7a5a608d6730bf53152dfa7288970",
|
||||||
|
"seqno": 14451686
|
||||||
|
},
|
||||||
|
"service": {
|
||||||
|
"entropy": "QIvRp0qNOIZFplyzXERiwp75",
|
||||||
|
"hostname": "masser.keybase.pub",
|
||||||
|
"protocol": "https:"
|
||||||
|
},
|
||||||
|
"type": "web_service_binding",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"name": "keybase.io go client",
|
||||||
|
"version": "4.3.1"
|
||||||
|
},
|
||||||
|
"ctime": 1580587641,
|
||||||
|
"expire_in": 504576000,
|
||||||
|
"prev": "c99f66ba0bfa7ff6be736e24691c8007b0db41b6036b7f1e5f9335ab7f3550f4",
|
||||||
|
"seqno": 34,
|
||||||
|
"tag": "signature"
|
||||||
|
}
|
||||||
|
|
||||||
|
which yields the signature:
|
||||||
|
|
||||||
|
hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEg+1hTInDCYaAK4J7z7kew3PdlkrSFFll3Mp4J7ExPyGkKp3BheWxvYWTESpcCIsQgyZ9mugv6f/a+c24kaRyAB7DbQbYDa38eX5M1q381UPTEIH8FtgpzXt/JJxrhAYknW7xL4+K57JBJnbhFpXWAfEH5AgHCo3NpZ8RApEnmCcZApdOTf+XAHRbbgoysAZHfyTHamWC/BEHKuqEf/BlUFSxo6ASa/YC+Y2sMJfW2iRFlP2bs822TyJTBA6hzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEINm3yJdAbmbGJd03EnpuKVh2JzkI8WkngbWbAxbbR0MMo3RhZ80CAqd2ZXJzaW9uAQ==
|
||||||
|
|
||||||
|
And finally, I am proving ownership of this host by posting or
|
||||||
|
appending to this document.
|
||||||
|
|
||||||
|
View my publicly-auditable identity here: https://keybase.io/masser
|
||||||
|
|
||||||
|
===================================================================
|
||||||
|
https://keybase.io/masser
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
I hereby claim:
|
||||||
|
|
||||||
|
* I am an admin of https://rauhala.info
|
||||||
|
* I am masser (https://keybase.io/masser) on keybase.
|
||||||
|
* I have a public key ASD7WFMicMJhoArgnvPuR7Dc92WStIUWWXcyngnsTE_IaQo
|
||||||
|
|
||||||
|
To do so, I am signing this object:
|
||||||
|
|
||||||
|
{
|
||||||
|
"body": {
|
||||||
|
"key": {
|
||||||
|
"eldest_kid": "01202eb5d3b8d2fc63e4bc9c6edefb3a38f1043dfdc44a7268393951fe0ea5214cf00a",
|
||||||
|
"host": "keybase.io",
|
||||||
|
"kid": "0120fb58532270c261a00ae09ef3ee47b0dcf76592b485165977329e09ec4c4fc8690a",
|
||||||
|
"uid": "b01557931e13482e9e646593a64e9119",
|
||||||
|
"username": "masser"
|
||||||
|
},
|
||||||
|
"merkle_root": {
|
||||||
|
"ctime": 1580591326,
|
||||||
|
"hash": "2b1b8c5e3de98e3e80ec4b4c28270066744d5d0982c23e4c13ec97d9311e32996b0cc39ab07f51a2418f833d3f03c0219c7ebd8cdc9c99ac09d3f88d0b6129d1",
|
||||||
|
"hash_meta": "0e85fd563e1159d6c5b32f5c424d4c97101c3a11346a0dad77bdbf043ca4f287",
|
||||||
|
"seqno": 14452336
|
||||||
|
},
|
||||||
|
"service": {
|
||||||
|
"entropy": "7UNm+4us1fZE3TRssgGatBC2",
|
||||||
|
"hostname": "rauhala.info",
|
||||||
|
"protocol": "https:"
|
||||||
|
},
|
||||||
|
"type": "web_service_binding",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"name": "keybase.io go client",
|
||||||
|
"version": "4.3.1"
|
||||||
|
},
|
||||||
|
"ctime": 1580591336,
|
||||||
|
"expire_in": 504576000,
|
||||||
|
"prev": "ed5dd9b2571746743f37dd55f59235bd46d7f7de01a2bafba14b9c0929751aee",
|
||||||
|
"seqno": 35,
|
||||||
|
"tag": "signature"
|
||||||
|
}
|
||||||
|
|
||||||
|
which yields the signature:
|
||||||
|
|
||||||
|
hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEg+1hTInDCYaAK4J7z7kew3PdlkrSFFll3Mp4J7ExPyGkKp3BheWxvYWTESpcCI8Qg7V3ZslcXRnQ/N91V9ZI1vUbX994Borr7oUucCSl1Gu7EIJld9Pai+YxhSvZUIA+ZyzwEEbpy98uEqp975YKAEdz9AgHCo3NpZ8RAtksa9w0UQrwg8dPGEzahht+hY4QKQthbZ/yWuOSmfA3C5E1/KorbIEi+tcy5lkSJqNbZRSqBgpl9zerYw5bbAahzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEICy29OKKJc5LUELFLo8kW15N3zex43ussKLFeGLhTDapo3RhZ80CAqd2ZXJzaW9uAQ==
|
||||||
|
|
||||||
|
And finally, I am proving ownership of this host by posting or
|
||||||
|
appending to this document.
|
||||||
|
|
||||||
|
View my publicly-auditable identity here: https://keybase.io/masser
|
||||||
|
|
||||||
|
===================================================================================================================================
|
21
release.nix
@ -1,21 +0,0 @@
|
|||||||
{ pkgs ? import <nixpkgs> {} }:
|
|
||||||
|
|
||||||
let
|
|
||||||
haskellPackages = pkgs.haskellPackages;
|
|
||||||
site = pkgs.callPackage ./default.nix {};
|
|
||||||
shell = pkgs.buildEnv {
|
|
||||||
name = "site-shell";
|
|
||||||
paths = [];
|
|
||||||
buildInputs = [
|
|
||||||
haskellPackages.ghcid
|
|
||||||
haskellPackages.hasktags
|
|
||||||
(haskellPackages.ghcWithHoogle (h: site.buildInputs ++ site.propagatedBuildInputs))
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
|
||||||
|
|
||||||
{
|
|
||||||
site = site;
|
|
||||||
shell = shell;
|
|
||||||
}
|
|
71
site.hs
@ -1,71 +0,0 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
import Data.List (sortOn)
|
|
||||||
import Data.Time (defaultTimeLocale, formatTime)
|
|
||||||
import Hakyll
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
main :: IO ()
|
|
||||||
main = hakyllWith defaultConfiguration{ deployCommand = "ipfs add -Q -r _site" } $do
|
|
||||||
match "images/*" $ do
|
|
||||||
route idRoute
|
|
||||||
compile copyFileCompiler
|
|
||||||
|
|
||||||
match "resources/*" $ do
|
|
||||||
route idRoute
|
|
||||||
compile copyFileCompiler
|
|
||||||
|
|
||||||
match "css/*" $ do
|
|
||||||
route idRoute
|
|
||||||
compile compressCssCompiler
|
|
||||||
|
|
||||||
match "js/*" $ do
|
|
||||||
route idRoute
|
|
||||||
compile compressCssCompiler
|
|
||||||
|
|
||||||
match (fromList ["index.markdown", "contact.markdown"]) $ do
|
|
||||||
route $ setExtension "html"
|
|
||||||
compile $ pandocCompiler
|
|
||||||
>>= loadAndApplyTemplate "templates/default.html" defaultContext
|
|
||||||
>>= relativizeUrls
|
|
||||||
|
|
||||||
match "posts/*" $ do
|
|
||||||
route $ setExtension "html"
|
|
||||||
compile $ pandocCompiler
|
|
||||||
>>= loadAndApplyTemplate "templates/post.html" postCtx
|
|
||||||
>>= loadAndApplyTemplate "templates/default.html" postCtx
|
|
||||||
>>= relativizeUrls
|
|
||||||
|
|
||||||
create ["posts.html"] $ do
|
|
||||||
route idRoute
|
|
||||||
compile $ do
|
|
||||||
posts <- modFirst =<< loadAll "posts/*"
|
|
||||||
let archiveCtx =
|
|
||||||
listField "posts" postCtx (return posts) <>
|
|
||||||
constField "title" "Posts" <>
|
|
||||||
defaultContext
|
|
||||||
|
|
||||||
makeItem ""
|
|
||||||
>>= loadAndApplyTemplate "templates/guides.html" archiveCtx
|
|
||||||
>>= loadAndApplyTemplate "templates/default.html" archiveCtx
|
|
||||||
>>= relativizeUrls
|
|
||||||
|
|
||||||
|
|
||||||
match "templates/*" $ compile templateBodyCompiler
|
|
||||||
|
|
||||||
modFirst :: [Item a] -> Compiler [Item a]
|
|
||||||
modFirst = fmap reverse . modified
|
|
||||||
where
|
|
||||||
modified = sortByM (getItemModificationTime . itemIdentifier)
|
|
||||||
sortByM f xs = map fst . sortOn snd <$> mapM (\x -> (,) x <$> f x) xs
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
postCtx :: Context String
|
|
||||||
postCtx =
|
|
||||||
dateField "date" "%B %e, %Y" <>
|
|
||||||
modifiedField "modified" "%B %e, %Y" <>
|
|
||||||
defaultContext
|
|
||||||
where
|
|
||||||
modifiedField key format = field key $ \i -> do
|
|
||||||
time <- getItemModificationTime $ itemIdentifier i
|
|
||||||
return $ formatTime defaultTimeLocale format time
|
|
249
site/.stylish-haskell.yaml
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
# stylish-haskell configuration file
|
||||||
|
# ==================================
|
||||||
|
|
||||||
|
# The stylish-haskell tool is mainly configured by specifying steps. These steps
|
||||||
|
# are a list, so they have an order, and one specific step may appear more than
|
||||||
|
# once (if needed). Each file is processed by these steps in the given order.
|
||||||
|
steps:
|
||||||
|
# Convert some ASCII sequences to their Unicode equivalents. This is disabled
|
||||||
|
# by default.
|
||||||
|
# - unicode_syntax:
|
||||||
|
# # In order to make this work, we also need to insert the UnicodeSyntax
|
||||||
|
# # language pragma. If this flag is set to true, we insert it when it's
|
||||||
|
# # not already present. You may want to disable it if you configure
|
||||||
|
# # language extensions using some other method than pragmas. Default:
|
||||||
|
# # true.
|
||||||
|
# add_language_pragma: true
|
||||||
|
|
||||||
|
# Align the right hand side of some elements. This is quite conservative
|
||||||
|
# and only applies to statements where each element occupies a single
|
||||||
|
# line.
|
||||||
|
- simple_align:
|
||||||
|
cases: false
|
||||||
|
top_level_patterns: false
|
||||||
|
records: false
|
||||||
|
|
||||||
|
# Import cleanup
|
||||||
|
- imports:
|
||||||
|
# There are different ways we can align names and lists.
|
||||||
|
#
|
||||||
|
# - global: Align the import names and import list throughout the entire
|
||||||
|
# file.
|
||||||
|
#
|
||||||
|
# - file: Like global, but don't add padding when there are no qualified
|
||||||
|
# imports in the file.
|
||||||
|
#
|
||||||
|
# - group: Only align the imports per group (a group is formed by adjacent
|
||||||
|
# import lines).
|
||||||
|
#
|
||||||
|
# - none: Do not perform any alignment.
|
||||||
|
#
|
||||||
|
# Default: global.
|
||||||
|
align: none
|
||||||
|
|
||||||
|
# The following options affect only import list alignment.
|
||||||
|
#
|
||||||
|
# List align has following options:
|
||||||
|
#
|
||||||
|
# - after_alias: Import list is aligned with end of import including
|
||||||
|
# 'as' and 'hiding' keywords.
|
||||||
|
#
|
||||||
|
# > import qualified Data.List as List (concat, foldl, foldr, head,
|
||||||
|
# > init, last, length)
|
||||||
|
#
|
||||||
|
# - with_alias: Import list is aligned with start of alias or hiding.
|
||||||
|
#
|
||||||
|
# > import qualified Data.List as List (concat, foldl, foldr, head,
|
||||||
|
# > init, last, length)
|
||||||
|
#
|
||||||
|
# - new_line: Import list starts always on new line.
|
||||||
|
#
|
||||||
|
# > import qualified Data.List as List
|
||||||
|
# > (concat, foldl, foldr, head, init, last, length)
|
||||||
|
#
|
||||||
|
# Default: after_alias
|
||||||
|
list_align: new_line
|
||||||
|
|
||||||
|
# Right-pad the module names to align imports in a group:
|
||||||
|
#
|
||||||
|
# - true: a little more readable
|
||||||
|
#
|
||||||
|
# > import qualified Data.List as List (concat, foldl, foldr,
|
||||||
|
# > init, last, length)
|
||||||
|
# > import qualified Data.List.Extra as List (concat, foldl, foldr,
|
||||||
|
# > init, last, length)
|
||||||
|
#
|
||||||
|
# - false: diff-safe
|
||||||
|
#
|
||||||
|
# > import qualified Data.List as List (concat, foldl, foldr, init,
|
||||||
|
# > last, length)
|
||||||
|
# > import qualified Data.List.Extra as List (concat, foldl, foldr,
|
||||||
|
# > init, last, length)
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
pad_module_names: false
|
||||||
|
|
||||||
|
# Long list align style takes effect when import is too long. This is
|
||||||
|
# determined by 'columns' setting.
|
||||||
|
#
|
||||||
|
# - inline: This option will put as much specs on same line as possible.
|
||||||
|
#
|
||||||
|
# - new_line: Import list will start on new line.
|
||||||
|
#
|
||||||
|
# - new_line_multiline: Import list will start on new line when it's
|
||||||
|
# short enough to fit to single line. Otherwise it'll be multiline.
|
||||||
|
#
|
||||||
|
# - multiline: One line per import list entry.
|
||||||
|
# Type with constructor list acts like single import.
|
||||||
|
#
|
||||||
|
# > import qualified Data.Map as M
|
||||||
|
# > ( empty
|
||||||
|
# > , singleton
|
||||||
|
# > , ...
|
||||||
|
# > , delete
|
||||||
|
# > )
|
||||||
|
#
|
||||||
|
# Default: inline
|
||||||
|
long_list_align: new_line_multiline
|
||||||
|
|
||||||
|
# Align empty list (importing instances)
|
||||||
|
#
|
||||||
|
# Empty list align has following options
|
||||||
|
#
|
||||||
|
# - inherit: inherit list_align setting
|
||||||
|
#
|
||||||
|
# - right_after: () is right after the module name:
|
||||||
|
#
|
||||||
|
# > import Vector.Instances ()
|
||||||
|
#
|
||||||
|
# Default: inherit
|
||||||
|
empty_list_align: inherit
|
||||||
|
|
||||||
|
# List padding determines indentation of import list on lines after import.
|
||||||
|
# This option affects 'long_list_align'.
|
||||||
|
#
|
||||||
|
# - <integer>: constant value
|
||||||
|
#
|
||||||
|
# - module_name: align under start of module name.
|
||||||
|
# Useful for 'file' and 'group' align settings.
|
||||||
|
list_padding: 7
|
||||||
|
|
||||||
|
# Separate lists option affects formatting of import list for type
|
||||||
|
# or class. The only difference is single space between type and list
|
||||||
|
# of constructors, selectors and class functions.
|
||||||
|
#
|
||||||
|
# - true: There is single space between Foldable type and list of it's
|
||||||
|
# functions.
|
||||||
|
#
|
||||||
|
# > import Data.Foldable (Foldable (fold, foldl, foldMap))
|
||||||
|
#
|
||||||
|
# - false: There is no space between Foldable type and list of it's
|
||||||
|
# functions.
|
||||||
|
#
|
||||||
|
# > import Data.Foldable (Foldable(fold, foldl, foldMap))
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
separate_lists: false
|
||||||
|
|
||||||
|
# Space surround option affects formatting of import lists on a single
|
||||||
|
# line. The only difference is single space after the initial
|
||||||
|
# parenthesis and a single space before the terminal parenthesis.
|
||||||
|
#
|
||||||
|
# - true: There is single space associated with the enclosing
|
||||||
|
# parenthesis.
|
||||||
|
#
|
||||||
|
# > import Data.Foo ( foo )
|
||||||
|
#
|
||||||
|
# - false: There is no space associated with the enclosing parenthesis
|
||||||
|
#
|
||||||
|
# > import Data.Foo (foo)
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
space_surround: false
|
||||||
|
|
||||||
|
# Language pragmas
|
||||||
|
- language_pragmas:
|
||||||
|
# We can generate different styles of language pragma lists.
|
||||||
|
#
|
||||||
|
# - vertical: Vertical-spaced language pragmas, one per line.
|
||||||
|
#
|
||||||
|
# - compact: A more compact style.
|
||||||
|
#
|
||||||
|
# - compact_line: Similar to compact, but wrap each line with
|
||||||
|
# `{-#LANGUAGE #-}'.
|
||||||
|
#
|
||||||
|
# Default: vertical.
|
||||||
|
style: vertical
|
||||||
|
|
||||||
|
# Align affects alignment of closing pragma brackets.
|
||||||
|
#
|
||||||
|
# - true: Brackets are aligned in same column.
|
||||||
|
#
|
||||||
|
# - false: Brackets are not aligned together. There is only one space
|
||||||
|
# between actual import and closing bracket.
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
align: false
|
||||||
|
|
||||||
|
# stylish-haskell can detect redundancy of some language pragmas. If this
|
||||||
|
# is set to true, it will remove those redundant pragmas. Default: true.
|
||||||
|
remove_redundant: true
|
||||||
|
|
||||||
|
# Replace tabs by spaces. This is disabled by default.
|
||||||
|
# - tabs:
|
||||||
|
# # Number of spaces to use for each tab. Default: 8, as specified by the
|
||||||
|
# # Haskell report.
|
||||||
|
# spaces: 8
|
||||||
|
|
||||||
|
# Remove trailing whitespace
|
||||||
|
- trailing_whitespace: {}
|
||||||
|
|
||||||
|
# Squash multiple spaces between the left and right hand sides of some
|
||||||
|
# elements into single spaces. Basically, this undoes the effect of
|
||||||
|
# simple_align but is a bit less conservative.
|
||||||
|
# - squash: {}
|
||||||
|
|
||||||
|
# A common setting is the number of columns (parts of) code will be wrapped
|
||||||
|
# to. Different steps take this into account. Default: 80.
|
||||||
|
columns: 80
|
||||||
|
|
||||||
|
# By default, line endings are converted according to the OS. You can override
|
||||||
|
# preferred format here.
|
||||||
|
#
|
||||||
|
# - native: Native newline format. CRLF on Windows, LF on other OSes.
|
||||||
|
#
|
||||||
|
# - lf: Convert to LF ("\n").
|
||||||
|
#
|
||||||
|
# - crlf: Convert to CRLF ("\r\n").
|
||||||
|
#
|
||||||
|
# Default: native.
|
||||||
|
newline: native
|
||||||
|
|
||||||
|
# Sometimes, language extensions are specified in a cabal file or from the
|
||||||
|
# command line instead of using language pragmas in the file. stylish-haskell
|
||||||
|
# needs to be aware of these, so it can parse the file correctly.
|
||||||
|
#
|
||||||
|
# No language extensions are enabled by default.
|
||||||
|
language_extensions:
|
||||||
|
- RecordWildCards
|
||||||
|
- TemplateHaskell
|
||||||
|
- QuasiQuotes
|
||||||
|
- LambdaCase
|
||||||
|
- TupleSections
|
||||||
|
- MultiParamTypeClasses
|
||||||
|
- TypeApplications
|
||||||
|
- DataKinds
|
||||||
|
- TypeFamilies
|
||||||
|
- FlexibleContexts
|
||||||
|
- NamedFieldPuns
|
||||||
|
- MultiWayIf
|
||||||
|
- PolyKinds
|
||||||
|
- ExplicitForAll
|
||||||
|
- FunctionalDependencies
|
||||||
|
- ExplicitNamespaces
|
||||||
|
- ScopedTypeVariables
|
||||||
|
- ExistentialQuantification
|
||||||
|
- InstanceSigs
|
||||||
|
- GeneralizedNewtypeDeriving
|
||||||
|
- BangPatterns
|
||||||
|
|
107
site/app/site.hs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE StrictData #-}
|
||||||
|
import Hakyll
|
||||||
|
|
||||||
|
import Data.List
|
||||||
|
(isPrefixOf, isSuffixOf)
|
||||||
|
|
||||||
|
import System.FilePath
|
||||||
|
(takeFileName)
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
main :: IO ()
|
||||||
|
main = hakyllWith defaultConfiguration{ignoreFile = ignore} $ do
|
||||||
|
match "well-known/*" $ do
|
||||||
|
route (customRoute (prepend '.'. toFilePath))
|
||||||
|
compile copyFileCompiler
|
||||||
|
|
||||||
|
match "images/*" $ do
|
||||||
|
route idRoute
|
||||||
|
compile copyFileCompiler
|
||||||
|
|
||||||
|
match "resources/*" $ do
|
||||||
|
route idRoute
|
||||||
|
compile copyFileCompiler
|
||||||
|
|
||||||
|
match "css/*" $ do
|
||||||
|
route idRoute
|
||||||
|
compile compressCssCompiler
|
||||||
|
|
||||||
|
match "js/*" $ do
|
||||||
|
route idRoute
|
||||||
|
compile compressCssCompiler
|
||||||
|
|
||||||
|
match "posts/*" $ do
|
||||||
|
route $ setExtension "html"
|
||||||
|
compile $ pandocCompiler
|
||||||
|
>>= loadAndApplyTemplate "templates/post.html" postContext
|
||||||
|
>>= loadAndApplyTemplate "templates/default.html" defaultContext
|
||||||
|
>>= relativizeUrls
|
||||||
|
|
||||||
|
match "projects/*" $ do
|
||||||
|
route $ setExtension "html"
|
||||||
|
compile $ pandocCompiler
|
||||||
|
>>= loadAndApplyTemplate "templates/project.html" postContext
|
||||||
|
>>= loadAndApplyTemplate "templates/default.html" defaultContext
|
||||||
|
>>= relativizeUrls
|
||||||
|
|
||||||
|
|
||||||
|
match (fromList ["index.markdown", "contact.markdown"]) $ do
|
||||||
|
route $ setExtension "html"
|
||||||
|
compile $ pandocCompiler
|
||||||
|
>>= loadAndApplyTemplate "templates/default.html" defaultContext
|
||||||
|
>>= relativizeUrls
|
||||||
|
|
||||||
|
archive $ Archive { output = "posts.html"
|
||||||
|
, input = "posts/*"
|
||||||
|
, title = "Posts"
|
||||||
|
, template = "templates/post-list.html"
|
||||||
|
, context = postContext
|
||||||
|
}
|
||||||
|
|
||||||
|
archive $ Archive { output = "projects.html"
|
||||||
|
, input = "projects/*"
|
||||||
|
, title = "Projects"
|
||||||
|
, template = "templates/project-list.html"
|
||||||
|
, context = postContext
|
||||||
|
}
|
||||||
|
|
||||||
|
match "templates/*" $ compile templateBodyCompiler
|
||||||
|
where
|
||||||
|
|
||||||
|
postContext :: Context String
|
||||||
|
postContext = dateField "date" "%B %e, %Y" <> defaultContext
|
||||||
|
ignore :: FilePath -> Bool
|
||||||
|
ignore path = any ($ takeFileName path)
|
||||||
|
[ ("." `isPrefixOf`)
|
||||||
|
, ("#" `isPrefixOf`)
|
||||||
|
, ("~" `isSuffixOf`)
|
||||||
|
, (".swp" `isSuffixOf`)
|
||||||
|
]
|
||||||
|
|
||||||
|
data Archive
|
||||||
|
= Archive { output :: Identifier
|
||||||
|
, input :: Pattern
|
||||||
|
, title :: String
|
||||||
|
, template :: Identifier
|
||||||
|
, context :: Context String
|
||||||
|
}
|
||||||
|
|
||||||
|
archive :: Archive -> Rules ()
|
||||||
|
archive Archive{..} = create [output] $ do
|
||||||
|
route idRoute
|
||||||
|
compile $ do
|
||||||
|
let itemsContext =
|
||||||
|
listField "items" context items
|
||||||
|
<> constField "title" title
|
||||||
|
<> defaultContext
|
||||||
|
items = recentFirst =<< loadAll input
|
||||||
|
makeItem ""
|
||||||
|
>>= loadAndApplyTemplate template itemsContext
|
||||||
|
>>= loadAndApplyTemplate "templates/default.html" itemsContext
|
||||||
|
>>= relativizeUrls
|
||||||
|
|
||||||
|
prepend :: a -> [a] -> [a]
|
||||||
|
prepend = (:)
|
10
site/default.nix
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{ mkDerivation, base, filepath, hakyll, lib, time }:
|
||||||
|
mkDerivation {
|
||||||
|
pname = "site";
|
||||||
|
version = "0.1.0.0";
|
||||||
|
src = ./.;
|
||||||
|
isLibrary = false;
|
||||||
|
isExecutable = true;
|
||||||
|
executableHaskellDepends = [ base filepath hakyll time ];
|
||||||
|
license = lib.licenses.bsd3;
|
||||||
|
}
|
@ -9,8 +9,10 @@ maintainer: mats.rauhala@iki.fi
|
|||||||
|
|
||||||
executable site
|
executable site
|
||||||
main-is: site.hs
|
main-is: site.hs
|
||||||
|
hs-source-dirs: app
|
||||||
build-depends: base == 4.*
|
build-depends: base == 4.*
|
||||||
, hakyll >= 4.10
|
, hakyll >= 4.10
|
||||||
, time
|
, time
|
||||||
|
, filepath
|
||||||
ghc-options: -threaded
|
ghc-options: -threaded
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
@ -1,3 +0,0 @@
|
|||||||
Me writing out interesting ideas and experiments.
|
|
||||||
|
|
||||||
$partial("templates/post-list.html")$
|
|
@ -1,7 +0,0 @@
|
|||||||
<ul>
|
|
||||||
$for(posts)$
|
|
||||||
<li>
|
|
||||||
<a href="$url$">$title$</a> - $modified$
|
|
||||||
</li>
|
|
||||||
$endfor$
|
|
||||||
</ul>
|
|
@ -1,11 +0,0 @@
|
|||||||
<article>
|
|
||||||
<section class="header">
|
|
||||||
Posted on $date$, modified on $modified$
|
|
||||||
$if(author)$
|
|
||||||
by $author$
|
|
||||||
$endif$
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
$body$
|
|
||||||
</section>
|
|
||||||
</article>
|
|