Compare commits
2 Commits
a49af625bf
...
books
Author | SHA1 | Date | |
---|---|---|---|
2e9a9155f8 | |||
1ab9c3af46 |
2
.gitignore
vendored
@ -1,5 +1,3 @@
|
||||
_site/
|
||||
_cache/
|
||||
dist/
|
||||
dist-newstyle/
|
||||
result*
|
||||
|
2
attributions.md
Normal file
@ -0,0 +1,2 @@
|
||||
- Book by rivercon from the Noun Project
|
||||
- [Git logo](https://git-scm.com/downloads/logos) by Jason Long
|
7
books.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<books>
|
||||
<book>
|
||||
<date>2019-05-28</date>
|
||||
<title>Uppo-Nallen talviturkki</title>
|
||||
<status>read</status>
|
||||
</book>
|
||||
</books>
|
5
books/2014-07-10-pandoras-star.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Uppo-Nalle ja Nukku-Ukko
|
||||
status: reading
|
||||
---
|
||||
|
4
books/2016-11-04-the-fractal-prince.md
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: The Fractal Prince
|
||||
status: reading
|
||||
---
|
4
books/2019-05-28-nukku-ukko.md
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Uppo-Nalle ja Nukku-Ukko
|
||||
status: reading
|
||||
---
|
5
books/2019-05-28-upponallen-talviturkki.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Uppo-Nallen talviturkki
|
||||
status: read
|
||||
---
|
||||
|
@ -8,4 +8,3 @@ I live in Espoo Finland. You can contact me on any of the following services.
|
||||
- **Slack**: masser@functionalprogramming.slack.com
|
||||
- **IRC**: MasseR@freenode
|
||||
- **Mastodon**: MasseR@mastodon.social
|
||||
- **Keybase**: [https://keybase.io/MasseR](https://keybase.io/MasseR)
|
@ -51,6 +51,15 @@ article .header {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
display: inline-flex;
|
||||
align-self: center;
|
||||
top: .40em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 319px) {
|
||||
body {
|
||||
width: 90%;
|
||||
@ -109,12 +118,6 @@ article .header {
|
||||
}
|
||||
}
|
||||
|
||||
.projectlist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
body {
|
||||
width: 60rem;
|
@ -1,8 +1,3 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
{ haskellPackages, haskell }:
|
||||
|
||||
with pkgs;
|
||||
|
||||
rec {
|
||||
site = haskellPackages.callPackage ./site {};
|
||||
rauhala-info = callPackage ./rauhala.info { site = site; };
|
||||
}
|
||||
haskell.lib.disableSharedExecutables (haskellPackages.callCabal2nix "site" ./. {})
|
||||
|
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 |
1
images/book_read.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="read" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 56 70" style="enable-background:new 0 0 56 56;" xml:space="preserve"><path d="M48.2026367,4.003418c0-0.5522461-0.4477539-1-1-1H17.7766724C17.7706909,3.0032959,17.7657471,3,17.7597656,3 s-0.0109253,0.0032959-0.0169067,0.003418h-4.8380737c-2.8188477,0-5.1118164,2.4799805-5.1118164,5.5288086v38.4257812 c0,0.0093994,0.005127,0.0172119,0.0053711,0.0265503C7.8088379,50.3031616,10.303833,53,13.3701172,53h33.8369141 c0.3862305,0,0.737793-0.2226562,0.9033203-0.5712891c0.1655273-0.3491211,0.1152344-0.762207-0.1289062-1.0615234 c-2.0512695-2.5102539-2.0512695-6.296875,0-8.8071289c0.2003784-0.2456665,0.262085-0.5665894,0.1891479-0.8672485 c0.0089111-0.053772,0.0320435-0.102356,0.0320435-0.1586304V4.003418z M46.2026367,40.9272461H18.7597656V5.003418h27.4428711 V40.9272461z M12.9047852,5.003418h3.8549805v35.9238281h-3.3896484c-1.3610229,0-2.6083374,0.5328979-3.5771484,1.4140625 V8.5322266C9.7929688,6.5864258,11.1889648,5.003418,12.9047852,5.003418z M45.3833008,51H13.3701172 c-1.9702148,0-3.5727539-1.8105469-3.5727539-4.0361328s1.6025391-4.0366211,3.5727539-4.0366211h32.0131836 C44.1420898,45.4414062,44.1420898,48.4858398,45.3833008,51z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
images/book_reading.svg
Normal file
After Width: | Height: | Size: 6.8 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
images/profile.jpg
Normal file
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
@ -19,8 +19,8 @@ My [GPG key](./resources/2104943D6033C.txt)
|
||||
- Working on an internal Haskell based tool at Relex Oy
|
||||
- Some contributions to [darcshub](https://hub.darcs.net/)
|
||||
- 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.
|
||||
- My [Github profile](https://github.com/MasseR)
|
||||
- My [Gitea profile](https://git.rauhala.info/MasseR)
|
||||
- **Systems integrations**
|
||||
- Working as a consultant from Avoltus Oy to different companies using
|
||||
[Mulesoft](https://developer.mulesoft.com/). Systems include webshops,
|
||||
@ -28,4 +28,4 @@ My [GPG key](./resources/2104943D6033C.txt)
|
||||
- **Java**
|
||||
- Bunch of different smaller projects while working at Avoltus Oy.
|
||||
- **Other**
|
||||
- I was involved in creating an email advertising platform.
|
||||
- I was involved in creating an email solicit platform.
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"url": "https://github.com/NixOS/nixpkgs",
|
||||
"rev": "7e1f60dfbba67b975d1a77d710a6f1437fd9709c",
|
||||
"date": "2020-02-01T09:20:37-05:00",
|
||||
"sha256": "0vk55459iljr5dzwnr5661l44b0wdc15952lk2rjcmxr1620yr5v",
|
||||
"fetchSubmodules": false
|
||||
}
|
148
posts/guides/demobot.md
Normal file
@ -0,0 +1,148 @@
|
||||
---
|
||||
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
|
||||
~~~
|
@ -1 +0,0 @@
|
||||
use nix
|
2
rauhala.info/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
_site
|
||||
_cache
|
@ -1,24 +0,0 @@
|
||||
{ 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: 25 KiB |
@ -1,23 +0,0 @@
|
||||
---
|
||||
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
|
||||
```
|
@ -1,10 +0,0 @@
|
||||
with (import <nixpkgs> {});
|
||||
|
||||
let site = haskellPackages.callPackage ../site {};
|
||||
|
||||
in
|
||||
|
||||
mkShell {
|
||||
buildInputs = [ site ];
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
<ul>
|
||||
$for(items)$
|
||||
<li>
|
||||
<a href="$url$">$title$</a> - $date$
|
||||
</li>
|
||||
$endfor$
|
||||
</ul>
|
@ -1,8 +0,0 @@
|
||||
<article>
|
||||
<section class="header">
|
||||
Posted on $date$
|
||||
</section>
|
||||
<section>
|
||||
$body$
|
||||
</section>
|
||||
</article>
|
@ -1,8 +0,0 @@
|
||||
<ul>
|
||||
$for(items)$
|
||||
<li>
|
||||
<div class="projectlist"><a href="$url$">$title$</a>$if(badge)$<img src="$badge$" />$endif$</div>
|
||||
</li>
|
||||
$endfor$
|
||||
</ul>
|
||||
|
@ -1,6 +0,0 @@
|
||||
<article>
|
||||
<section>
|
||||
$body$
|
||||
</section>
|
||||
</article>
|
||||
|
@ -1,111 +0,0 @@
|
||||
==================================================================
|
||||
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
|
||||
|
||||
===================================================================================================================================
|
22
release.nix
Normal file
@ -0,0 +1,22 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
let
|
||||
haskellPackages = pkgs.haskellPackages;
|
||||
site = pkgs.callPackage ./default.nix {};
|
||||
shell = pkgs.buildEnv {
|
||||
name = "site-shell";
|
||||
paths = [];
|
||||
buildInputs = with haskellPackages; [
|
||||
ghcid
|
||||
hasktags
|
||||
cabal-install
|
||||
(ghcWithHoogle (h: site.buildInputs ++ site.propagatedBuildInputs))
|
||||
];
|
||||
};
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
site = site;
|
||||
shell = shell;
|
||||
}
|
@ -9,10 +9,11 @@ maintainer: mats.rauhala@iki.fi
|
||||
|
||||
executable site
|
||||
main-is: site.hs
|
||||
hs-source-dirs: app
|
||||
build-depends: base == 4.*
|
||||
, hakyll >= 4.10
|
||||
, time
|
||||
, filepath
|
||||
, xml-conduit
|
||||
, xml-lens
|
||||
, lens
|
||||
ghc-options: -threaded
|
||||
default-language: Haskell2010
|
110
site.hs
Normal file
@ -0,0 +1,110 @@
|
||||
--------------------------------------------------------------------------------
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
import Data.Monoid (mappend)
|
||||
import Hakyll
|
||||
import Data.List (sortBy, sortOn)
|
||||
import Data.Time (formatTime, defaultTimeLocale)
|
||||
|
||||
data Book =
|
||||
Book { title :: String
|
||||
, date :: Day
|
||||
, status :: String }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
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 "books/*" $ do
|
||||
route $ setExtension "html"
|
||||
compile $ pandocCompiler
|
||||
>>= loadAndApplyTemplate "templates/post.html" postCtx
|
||||
>>= loadAndApplyTemplate "templates/default.html" postCtx
|
||||
>>= relativizeUrls
|
||||
|
||||
match "posts/incomplete/*" $ do
|
||||
route $ setExtension "html"
|
||||
compile $ pandocCompiler
|
||||
>>= loadAndApplyTemplate "templates/post.html" postCtx
|
||||
>>= loadAndApplyTemplate "templates/default.html" postCtx
|
||||
>>= relativizeUrls
|
||||
|
||||
match "posts/guides/*" $ do
|
||||
route $ setExtension "html"
|
||||
compile $ pandocCompiler
|
||||
>>= loadAndApplyTemplate "templates/post.html" postCtx
|
||||
>>= loadAndApplyTemplate "templates/default.html" postCtx
|
||||
>>= relativizeUrls
|
||||
|
||||
match "posts/brainstorming/*" $ do
|
||||
route $ setExtension "html"
|
||||
compile $ pandocCompiler
|
||||
>>= loadAndApplyTemplate "templates/post.html" postCtx
|
||||
>>= loadAndApplyTemplate "templates/default.html" postCtx
|
||||
>>= relativizeUrls
|
||||
|
||||
create ["books.html"] $ do
|
||||
route idRoute
|
||||
compile $ do
|
||||
books <- reverse <$> loadAll "books/*"
|
||||
let ctx = listField "books" postCtx (pure books) <>
|
||||
constField "title" "Books" <>
|
||||
defaultContext
|
||||
makeItem ""
|
||||
>>= loadAndApplyTemplate "templates/books.html" ctx
|
||||
>>= loadAndApplyTemplate "templates/default.html" ctx
|
||||
>>= relativizeUrls
|
||||
|
||||
create ["guides.html"] $ do
|
||||
route idRoute
|
||||
compile $ do
|
||||
posts <- modFirst =<< loadAll "posts/guides/*"
|
||||
let archiveCtx =
|
||||
listField "posts" postCtx (return posts) `mappend`
|
||||
constField "title" "Guides" `mappend`
|
||||
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" `mappend`
|
||||
modifiedField "modified" "%B %e, %Y" `mappend`
|
||||
defaultContext
|
||||
where
|
||||
modifiedField key format = field key $ \i -> do
|
||||
time <- getItemModificationTime $ itemIdentifier i
|
||||
return $ formatTime defaultTimeLocale format time
|
@ -1 +0,0 @@
|
||||
use nix
|
@ -1,249 +0,0 @@
|
||||
# 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
@ -1,107 +0,0 @@
|
||||
{-# 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 = (:)
|
@ -1,10 +0,0 @@
|
||||
{ mkDerivation, base, filepath, hakyll, stdenv, time }:
|
||||
mkDerivation {
|
||||
pname = "site";
|
||||
version = "0.1.0.0";
|
||||
src = ./.;
|
||||
isLibrary = false;
|
||||
isExecutable = true;
|
||||
executableHaskellDepends = [ base filepath hakyll time ];
|
||||
license = stdenv.lib.licenses.bsd3;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
with (import <nixpkgs> {});
|
||||
|
||||
let site = haskellPackages.callPackage ./. {};
|
||||
|
||||
in
|
||||
|
||||
mkShell {
|
||||
buildInputs = [
|
||||
ghcid
|
||||
stylish-haskell
|
||||
haskellPackages.cabal-install
|
||||
hlint
|
||||
(haskellPackages.ghcWithPackages (_: site.buildInputs ++ site.propagatedBuildInputs))
|
||||
];
|
||||
}
|
||||
|
9
templates/books.html
Normal file
@ -0,0 +1,9 @@
|
||||
A collection of books I have read lately.
|
||||
|
||||
<ul class="books">
|
||||
$for(books)$
|
||||
<li>
|
||||
<img src="/images/book_$status$.svg" class="icon" /><a href="$url$">$title$</a> - $date$
|
||||
</li>
|
||||
$endfor$
|
||||
</ul>
|
@ -10,15 +10,12 @@
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<!-- <div class="logo"> -->
|
||||
<!-- <a href="/">rauhala.info</a> -->
|
||||
<!-- </div> -->
|
||||
<div class="logo">
|
||||
<a href="/">rauhala.info</a>
|
||||
</div>
|
||||
<nav>
|
||||
<!-- Git logo from https://git-scm.com/downloads/logos -->
|
||||
<!-- 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="/contact.html">Contact</a>
|
||||
</nav>
|
||||
@ -31,7 +28,7 @@
|
||||
|
||||
<footer>
|
||||
Site proudly generated by
|
||||
<a href="http://jaspervdj.be/hakyll">Hakyll</a>
|
||||
<a href="http://jaspervdj.be/hakyll">Hakyll</a><br />
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
3
templates/guides.html
Normal file
@ -0,0 +1,3 @@
|
||||
A list of small and big guides.
|
||||
|
||||
$partial("templates/post-list.html")$
|
7
templates/post-list.html
Normal file
@ -0,0 +1,7 @@
|
||||
<ul>
|
||||
$for(posts)$
|
||||
<li>
|
||||
<a href="$url$">$title$</a> - $modified$
|
||||
</li>
|
||||
$endfor$
|
||||
</ul>
|
11
templates/post.html
Normal file
@ -0,0 +1,11 @@
|
||||
<article>
|
||||
<section class="header">
|
||||
Posted on $date$, modified on $modified$
|
||||
$if(author)$
|
||||
by $author$
|
||||
$endif$
|
||||
</section>
|
||||
<section>
|
||||
$body$
|
||||
</section>
|
||||
</article>
|