4 Commits

Author SHA1 Message Date
afcfc7bb07 wip 2019-03-27 23:00:36 +02:00
4c8df0c5df wip 2019-03-27 22:26:48 +02:00
58209a2c6e Better css 2019-03-27 22:23:43 +02:00
9d18db19a2 Refactor 2019-03-27 21:39:43 +02:00
46 changed files with 403 additions and 977 deletions

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
_site/ _site/
_cache/ _cache/
dist/ dist/
dist-newstyle/
result*

View File

@ -8,4 +8,3 @@ I live in Espoo Finland. You can contact me on any of the following services.
- **Slack**: masser@functionalprogramming.slack.com - **Slack**: masser@functionalprogramming.slack.com
- **IRC**: MasseR@freenode - **IRC**: MasseR@freenode
- **Mastodon**: MasseR@mastodon.social - **Mastodon**: MasseR@mastodon.social
- **Keybase**: [https://keybase.io/MasseR](https://keybase.io/MasseR)

View File

@ -39,18 +39,6 @@ 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;
@ -63,7 +51,6 @@ article .header {
text-decoration: none; text-decoration: none;
} }
@media (max-width: 319px) { @media (max-width: 319px) {
body { body {
width: 90%; width: 90%;
@ -120,40 +107,14 @@ 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: 100rem; width: 85rem;
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;

View File

@ -1,6 +1,18 @@
/* Generated by pandoc. */ /* Generated by pandoc. */
table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre.sourceCode
{ 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; }

View File

@ -1,8 +1,3 @@
{ pkgs ? import <nixpkgs> {} }: { haskellPackages, haskell }:
with pkgs; haskell.lib.disableSharedExecutables (haskellPackages.callCabal2nix "site" ./. {})
rec {
site = haskellPackages.callPackage ./site {};
rauhala-info = callPackage ./rauhala.info { site = site; };
}

View File

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 684 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 485 B

View File

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 684 B

BIN
images/profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -15,15 +15,12 @@ My [GPG key](./resources/2104943D6033C.txt)
### Notable experience and interests ### Notable experience and interests
- **Relex Oy** - **Haskell**
- Working as a Senior Software Developer at Relex Oy - Working on an internal Haskell based tool 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,
@ -31,4 +28,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 advertising platform. - I was involved in creating an email solicit platform.

22
js/api.js Normal file
View File

@ -0,0 +1,22 @@
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 Normal file
View File

@ -0,0 +1,10 @@
$(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 Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
{
"url": "https://github.com/NixOS/nixpkgs",
"rev": "7e1f60dfbba67b975d1a77d710a6f1437fd9709c",
"date": "2020-02-01T09:20:37-05:00",
"sha256": "0vk55459iljr5dzwnr5661l44b0wdc15952lk2rjcmxr1620yr5v",
"fetchSubmodules": false
}

78
posts/configs.md Normal file
View File

@ -0,0 +1,78 @@
---
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 Normal file
View 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
~~~

View File

@ -1 +0,0 @@
use nix

View File

@ -1,2 +0,0 @@
_site
_cache

View File

@ -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/
'';
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,254 +0,0 @@
---
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)
```

View File

@ -1,37 +0,0 @@
---
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
```

View File

@ -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
```

View File

@ -1,17 +0,0 @@
---
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>
```

View File

@ -1,10 +0,0 @@
with (import <nixpkgs> {});
let site = haskellPackages.callPackage ../site {};
in
mkShell {
buildInputs = [ site ];
}

View File

@ -1,7 +0,0 @@
<ul>
$for(items)$
<li>
<a href="$url$">$title$</a> - $date$
</li>
$endfor$
</ul>

View File

@ -1,8 +0,0 @@
<article>
<section class="header">
Posted on $date$
</section>
<section>
$body$
</section>
</article>

View File

@ -1,11 +0,0 @@
<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>

View File

@ -1,12 +0,0 @@
<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>

View File

@ -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
===================================================================================================================================

21
release.nix Normal file
View File

@ -0,0 +1,21 @@
{ 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;
}

View File

@ -9,10 +9,8 @@ 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

71
site.hs Normal file
View File

@ -0,0 +1,71 @@
{-# 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

View File

@ -1 +0,0 @@
use nix

View File

@ -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

View File

@ -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 = (:)

View File

@ -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;
}

View File

@ -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))
];
}

View File

@ -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>

3
templates/guides.html Normal file
View File

@ -0,0 +1,3 @@
Me writing out interesting ideas and experiments.
$partial("templates/post-list.html")$

7
templates/post-list.html Normal file
View File

@ -0,0 +1,7 @@
<ul>
$for(posts)$
<li>
<a href="$url$">$title$</a> - $modified$
</li>
$endfor$
</ul>

11
templates/post.html Normal file
View 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>