Compare commits

...

34 Commits

Author SHA1 Message Date
Mats Rauhala 29deb31729 Get rid of keybase 2022-08-18 11:33:07 +03:00
Mats Rauhala a1be811d95 Update my profile 2022-08-18 11:31:46 +03:00
Mats Rauhala bd41cf2f64 Update deps 2022-08-18 11:26:12 +03:00
Mats Rauhala e48ec6ce3d Simplify the build setup 2022-08-18 11:24:36 +03:00
Mats Rauhala b46cf0bf15 Less overlays 2022-08-18 10:57:08 +03:00
Mats Rauhala 3bc2738ef6 Replace easy-hls with haskell-language-server 2022-08-18 10:37:28 +03:00
Mats Rauhala 69a1f4d921 Remove old key 2021-11-26 23:40:55 +02:00
Mats Rauhala a762b8b6fe Build first 2021-11-26 23:40:33 +02:00
Mats Rauhala 394a511187 Update gpg key 2021-11-26 23:26:23 +02:00
Mats Rauhala a19640ec02 Deploying via ipfs? 2021-11-26 21:47:20 +02:00
Mats Rauhala c50529e44e Flakes 2021-11-26 16:52:39 +02:00
Mats Rauhala fb9eea52a6 Update contact 2021-11-26 15:05:16 +02:00
Mats Rauhala 95e9b07f31 New post, deriving quickcheck 2021-01-26 19:28:49 +02:00
Mats Rauhala f8bd7e928d Update the index page 2021-01-25 22:22:34 +02:00
Mats Rauhala 96f76a7751 Add zettelkast project 2021-01-25 22:03:32 +02:00
Mats Rauhala a0640e1c0a Add bidirectional project 2021-01-25 22:03:20 +02:00
Mats Rauhala 8727fd33a6 Sidebar element 2021-01-25 21:52:33 +02:00
Mats Rauhala 869ad66b21 Shuffle css elements 2021-01-25 21:24:15 +02:00
Mats Rauhala 97afd713df Link to github 2021-01-25 21:21:06 +02:00
Mats Rauhala a49af625bf Projects page 2021-01-25 21:11:14 +02:00
Mats Rauhala 7056ca822c Set up posts again 2021-01-25 20:35:41 +02:00
Mats Rauhala 7e290bee2f Ignore temp files 2021-01-25 19:35:21 +02:00
Mats Rauhala 59cf230683 Restyle 2021-01-25 19:30:49 +02:00
Mats Rauhala 637fc7646b Remove post 2021-01-25 19:26:59 +02:00
Mats Rauhala c62ff908af Reshuffle the files for better nix organization 2021-01-25 18:19:17 +02:00
Mats Rauhala 712b61059f Verify rauhala.info 2020-02-01 23:09:27 +02:00
Mats Rauhala 2ead146d6a Change wording 2020-02-01 23:07:59 +02:00
Mats Rauhala 0ac70566cd Add shell.nix 2020-02-01 22:49:20 +02:00
Mats Rauhala 809599d830 Add pre-built site 2020-02-01 22:49:12 +02:00
Mats Rauhala 2e2f7b9be1 Have the keybase proof 2020-02-01 22:25:52 +02:00
Mats Rauhala b0f0d15475 Disable the logo if I'm providing this through the keybase.pub 2020-02-01 22:23:31 +02:00
Mats Rauhala e951f7217f Add path to keybase 2020-02-01 21:59:34 +02:00
Mats Rauhala f7023b4431 Update the profile picture 2020-02-01 21:56:05 +02:00
Mats Rauhala 08ad6fb469 Pin to a specific version 2020-02-01 21:44:17 +02:00
44 changed files with 1495 additions and 500 deletions

2
.gitignore vendored
View File

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

1
cabal.project Normal file
View File

@ -0,0 +1 @@
packages: */*.cabal

View File

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

View File

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

43
flake.lock Normal file
View File

@ -0,0 +1,43 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1660646295,
"narHash": "sha256-V4G+egGRc3elXPTr7QLJ7r7yrYed0areIKDiIAlMLC8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "762b003329510ea855b4097a37511eb19c7077f0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

37
flake.nix Normal file
View File

@ -0,0 +1,37 @@
{
description = "rauhala.info";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
{
}
//
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
hp = pkgs.haskellPackages.override ( old: {
overrides = pkgs.lib.composeExtensions ( old.overrides or (_: _: {})) (f: p: {
build-rauhala-info = f.callPackage ./site {};
});
} );
in
rec {
packages.rauhala-info = pkgs.callPackage ./rauhala.info { site = hp.build-rauhala-info; };
defaultPackage = packages.rauhala-info;
devShell = hp.shellFor {
packages = h: [h.build-rauhala-info];
buildInputs = with pkgs; [
ghcid
cabal-install
stylish-haskell
entr
haskell-language-server
ipfs
];
};
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,31 +0,0 @@
---
title: Mats Rauhala
---
![](./images/profile.jpg)
I'm a software developer from southern Finland. I'm currently working as a
Haskell developer at Relex Oy involved in an internal tool. I also have
experience with systems integrations and backend web development.
I'm also an aviation enthusiast. I have a glider pilots license and a touring
motor glider pilots license (LAPL(S)+TMG).
My [GPG key](./resources/2104943D6033C.txt)
### Notable experience and interests
- **Haskell**
- 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.
- **Systems integrations**
- Working as a consultant from Avoltus Oy to different companies using
[Mulesoft](https://developer.mulesoft.com/). Systems include webshops,
different databases, CRMs and ERPs.
- **Java**
- Bunch of different smaller projects while working at Avoltus Oy.
- **Other**
- I was involved in creating an email solicit platform.

7
nixpkgs.json Normal file
View 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
}

View File

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

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

View 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

View File

@ -39,6 +39,18 @@ h2 {
font-size: 2rem;
}
.projectlist {
display: flex;
align-items: center;
justify-content: space-between;
}
article.blog {
/* display: flex; */
/* align-items: center; */
}
article .header {
font-size: 1.4rem;
font-style: italic;
@ -51,6 +63,7 @@ article .header {
text-decoration: none;
}
@media (max-width: 319px) {
body {
width: 90%;
@ -107,14 +120,40 @@ article .header {
display: inline;
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) {
body {
width: 60rem;
width: 100rem;
margin: 0 auto;
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 {
margin: 0 0 3rem;
padding: 1.2rem 0;

24
rauhala.info/default.nix Normal file
View 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/
'';
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,32 @@
---
title: Mats Rauhala
---
![](./images/profile.jpg)
I'm a software developer from southern Finland. I'm currently working as a
Haskell developer at Relex Oy. I also have experience with systems integrations
and backend web development.
My [GPG key](./resources/0x9DE6E04ED1918118.txt)
### Notable experience and interests
- **Relex Oy**
- Working as a Lead Software Developer at Relex Oy
- Transitioned from day to day code to a role more close to an architect
- 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 [xmonad](https://github.com/xmonad)
- **Systems integrations**
- Working as a consultant from Avoltus Oy to different companies using
[Mulesoft](https://developer.mulesoft.com/). Systems include webshops,
different databases, CRMs and ERPs.
- **Java**
- Bunch of different smaller projects while working at Avoltus Oy.
- **Other**
- I was involved in creating an email advertising platform.

View 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)
```

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

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

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

View File

@ -0,0 +1,514 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
AQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9
AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJico
KSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJ
ipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi
4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcI
CQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKR
obHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldY
WVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0
tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMB
AAIRAxEAPwCyTg0E0hNNJoAduo3UzNJmgCXdRuqLdS5oAl3UZqHNKDQBLmk3U0bm
PAJqUQSn+HH1oAaDS07ylXG6VfoKUvAo6Ox/KgBlOVHbopP4Ufago+WNR79aRrmR
v4yPpQBJ5D9ThR7mjy4h96XPsBVcyE9STTd2aALW+EHhCfqaQ3JH3VUfQVWzRuoA
laeRjyxpm4nrTM0ZoAXPvSE0lGaACikooAdmkJqb7Ow+8VX6mk2RL96Qn6CgCHNH
WpTJCg4j3f7xpv2plHyBV+goAFhkPOw/U04RcfM6j8c1A0zP95ifrUe7igC3iBOr
FvpxR50a/diGf9o5qpuozQMtG5fsQv8AujFMaQk8sT+NQbuetIXFAE2/FJ5gqBpQ
Opqu93GjYZ1H1NK4F8SDFJ5yjqa59tXW3lljdwW3fLg9ary6t+8HPWmB1IkB6U7N
YdjfGc/LWsHOKQE2aM1Fvpd1MRJuozUe6jdQA/dRmm5HrTSaAJM0VFmigCZnJ6k0
wvQTTD1oAUtmm5oNNNACk0maSigBc0E1DNMkERkkYKo7muZv/FD+Z5doAF/vkc0r
jOluLqK2j3zOFX3rmdS8Uk5jsxj/AGj/AErAvL+e8k/eyM+PWqpbB96B2LjatqDk
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
KWoXaAMg7ScUiU+ZWEw9sOMOTUl6ZwN31AbEnitrwb69W8wxHiQilEFMAUn0eWPV
pzLw9CHm+xryZ2/pLdGqaAoc2NUCtQnMbLmuoK20O04hLNzLay/J5rlponeVo6i0
ZrAae9p1UMOq1GlqUnvry5Ib1segir0S6oekelXi9ommIDWzhoGdGfqaSwQIQSLi
6K7dRN0dwSi6eltbUT5dtD1NYXRzIFJhdWhhbGEgKFdvcmsgYWRkcmVzcykgPG1h
dHMucmF1aGFsYUByZWxleHNvbHV0aW9ucy5jb20+iQJMBBMBCgA2FiEEldvmTC6m
TWsODIwvnebgTtGRgRgFAmEfo5cCGwMECwkIBwQVCgkIBRYCAwEAAh4BAheAAAoJ
EJ3m4E7RkYEYSSYP/1+euBleDqO0hEJ3GULsCoSKaocR0JjVC24ydbdrAH6476lv
9GK/XXGazusaTtB9c6+hWk7gby8vyl5V+6HR7oVsQOHuLN19yaPRc/Y9npmDMYL1
ekxzNgF5S1KP4P4pymCmSDB93MzMWKVVq48xfBULUDKs7nK5wsmhkVzBStWS/bvK
IqaMtvyxvv1QJe/N73yjseikp13k+NQx7O+O8tVlRZGq7egNOrNjNm3Qm5npUleG
FLzSUN7cEXvrK5foLYtv7JN9ew9To0Y+k7JRftuv8kTZl1WPslDyjtG2ckwQsus/
ElL7eKTZmmzVKWRpJ8h4NJQsbv3KVr2Fz2Z83VaLCZErWOvm1R03cRzVeqDp9js/
G4iuaTcrfNGdY5cUXdlLx8zYKdQP/skaZ1L2WfcrEJ/CaZuKAGhum81e1T/MgKYG
3ArZ0RNdtG3OJ2Ae/gVZN+28bH6AbVausdraP8alO8+UMLvIiUvEvzt7K2o8p5Av
6+YJ8CfnkuE68sQqZzHMdgu7VLApqIuIMuI/j9VJg345wVBqR07/xJ0PP6jODoHg
ZDPUS8QBFT86VevWYnptwPk5z32OTIpLcqkvgWqHZVTd3iAtZks09rlYgtA8Z/ec
5WtsYduR3+r/P1rn+GS/Ot7A0Q2fS0oW0dt6pzVkn3dbWrw8vLkLJKD5TUrttCVN
YXRzIFJhdWhhbGEgPG1hdHMucmF1aGFsYUBnbWFpbC5jb20+iQJMBBMBCgA2FiEE
ldvmTC6mTWsODIwvnebgTtGRgRgFAmEfo60CGwMECwkIBwQVCgkIBRYCAwEAAh4B
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
QhI/nB5QWaALtbgTQRN0qEGVSjA=
=/9Yv
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -10,20 +10,25 @@
</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>
</header>
<main role="main">
<div>
<h1>$title$</h1>
$body$
</div>
</main>
<footer>

View File

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

View File

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

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

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

View File

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

View File

@ -1,176 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=VUWT
-----END PGP PUBLIC KEY BLOCK-----

87
site.hs
View File

@ -1,87 +0,0 @@
--------------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid (mappend)
import Hakyll
import Data.List (sortBy, sortOn)
import Data.Time (formatTime, defaultTimeLocale)
--------------------------------------------------------------------------------
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/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 ["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

249
site/.stylish-haskell.yaml Normal file
View 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
View 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
View 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;
}

View File

@ -9,8 +9,10 @@ maintainer: mats.rauhala@iki.fi
executable site
main-is: site.hs
hs-source-dirs: app
build-depends: base == 4.*
, hakyll >= 4.10
, time
, filepath
ghc-options: -threaded
default-language: Haskell2010

33
support/deploy-rauhala-info.sh Executable file
View File

@ -0,0 +1,33 @@
#!@bash@/bin/bash
set -e
nix build .#rauhala-info
API=/ip4/127.0.0.1/tcp/5001
function upload() {
echo "Uploading.."
hash=$(ipfs --api $API add -r result/share --pin=false -Q)
}
function pin() {
echo "Pinning"
ipfs --api $API pin remote rm --service=pinata --cid="$hash"
ipfs --api $API pin remote add --service=pinata --name=rauhala.info "$hash"
}
function publish() {
echo "Updating name"
ipfs --api $API name publish --key=rauhala.info "$hash"
}
upload
pin &
publish &
wait

View File

@ -1,3 +0,0 @@
A list of small and big guides.
$partial("templates/post-list.html")$

View File

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

View File

@ -1,11 +0,0 @@
<article>
<section class="header">
Posted on $date$, modified on $modified$
$if(author)$
by $author$
$endif$
</section>
<section>
$body$
</section>
</article>