This is my emacs/haskell development environment.
Intero seems to work better with Stack than Dante.
Last Modified : 2017 Oct 26 (Thu) 08:21:13 by Harold Carr.
Get
.haskell-minimal-init.el
hc-haskell-dante.el
hc-haskell-intero.el
.haskell-minimal-init.el
from https://github.com/haroldcarr/emacs.
When the following starts, it will ask you whether to use Dante or Intero.
emacs -q -l <path to>/.haskell-minimal-init.el
project navigation, building, testing, …
Keybindings (subset)
C-c p !
: projectile-run-shell-command-in-rootC-c p c
: projectile-compile-projectC-c p f
: projectile-find-fileC-c p k
: projectile-kill-buffersC-c p t
: projectile-toggle-between-implementation-and-testTo see full list, if whick-key is install: press C-c p
and wait.
try:
C-c p f
C-p
and C-n
to navigate list of candidatesRET
to go to that filesome templates (to see full list: M-x yas/describe-tables
)
mod
|
adds a named (based on filepath) module declaration |
main
|
adds a Main module and main function
|
lang
|
adds a LANGUAGE pragma
|
opt
|
adds a OPTIONS_GHC pragma
|
try:
List.hs
mod
M-/
(hippie-expand)simple module
RET
tab
to accept default module name (or start typing)try:
M-x hayoo
(using Hayoo!)M-x hoogle
(using Hoogle)try:
M-x hayoo
or hoogle
f a -> Maybe c
RETtry:
types
C-c .
: dante-type-atC-c C-t
: intero-type-atinfo (shows definition and where defined, even if external)
C-c ,
: dante-infoC-c C-i
: intero-infoM-.
: jump to definition (both dante and intero)M-,
: return to previous location (both dante and intero)M-?
: xref-find-references (dante : TODO : DOES NOT WORK FOR ME)M-?
: intero-uses-at (TODO : DOES NOT WORK FOR ME)try:
List.hs
data List a = Cons a (List a) | Nil
deriving (Eq, Foldable, Show)
Foldable
C-c ! l
: flycheck-list-errorsFoldable
C-c /
: dante-auto-fixC-c C-r
: intero-apply-suggestionsLANGUAGE
pragma)try:
List.hs
cdr Nil = Nil
cdr (Cons _ xs) = xs
cdr
C-c ! l
: flycheck-list-errorsC-c /
: dante-auto-fixC-c C-r
: intero-apply-suggestionstry:
List.hs
-- | Returns the first element, if non-empty.
--
-- >>> car Nil
--
-- >>> car (Cons 'a' Nil)
car :: List a -> Maybe a
car xs = case xs of
Nil -> Nothing
Cons x _ -> Just x
C-c "
: dante-eval-blockIf dante starts acting weird, restart it.
M-x dante-list-buffers RET
M-x intero-list-buffers RET
d ;; mark process for deletion
x ;; kill it
q ;; quit process list
M-x dante-restart RET
M-x intero-list-buffers RET
try:
List.hs
import System.E
M-n
or M-p
: move through suggestionsE
until only : import System.
System
try:
List.hs
C-c p t
ListSpec.hs
C-c p c
: buildC-c p !
: runM-x haskell-mode-stylish-buffer
This exposition focuses on a blockchain technique often used in a block: Merkle Trees.
setup:
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
module Block where
import ClassyPrelude as CP hiding ((<|))
import Control.Monad.ST.Strict (runST)
import Crypto.Hash.SHA256 as C (hash)
import Data.ByteString.Char8 as BS (pack, replicate)
import Data.Char (chr)
import Data.Map.Strict as M
import Data.Sequence as S
import Data.STRef.Strict
import Test.Hspec
import Test.RandomStrings (randomASCII, randomWord)
{-# ANN module ("HLint: ignore Eta reduce"::String) #-}
{-# ANN module ("HLint: ignore Reduce duplication"::String) #-}
For this exposition, a block will be defined as:
data Block = Block
{ header :: ! BlockHeader
, txs :: ! Transactions
} deriving (Eq, Show)
where transactions are are an ordered sequence of data:
type Transactions = Seq Transaction
Here the “transactions” are uninterpreted opaque data:
type Transaction = ByteString
In a “real” blockchain, this is where “smart contracts” would be involved (a subject of a future exposition).
The most important part, for now, is the header:
data BlockHeader = BlockHeader
{ bPrevHash :: ! BHash -- ^ hash of previous block
, bMerkleRoot :: ! BHash -- ^ root hash of this block's transactions
, bTimestamp :: ! BTimestamp -- ^ when this block was created
} deriving (Eq, Show)
where
type BHash = ByteString
type BTimestamp = ByteString
The exposition The Chain in Blockchain explained how blocks are linked in chains using bPrevHash
etc. The above definitions are to relate the current exposition to the previous exposition. They are not futher used below. Here the focus is on bMerkleRoot
.
The merkle root is a hash of the hash of the bytes that make up each transaction.
The bytes of all transactions in the block are individually hashed, forming a list of hashes:
type HashDigest = ByteString
type Hashes = Seq HashDigest
txHashes :: Block -> Hashes
txHashes (Block _ transactions) = CP.map C.hash transactions
Then each each adjacent pair of hashes are concatenated and hashed again,
concatHash :: HashDigest -> HashDigest -> HashDigest
concatHash x y = C.hash (x <> y)
forming the parent of those pairs. This process is repeated on each level of parents until there is a single node. That node is the “merkle root”:
The createMerkleRoot
function below is written to mimic create_merkle
in Mastering Bitcoin (scroll down to “Example 7-1. Building a merkle tree”). That code is a modification of the “real” generate_merkel_root
code in libbitcoin
createMerkleRoot :: Hashes -> HashDigest
createMerkleRoot (viewl -> EmptyL) = nullHash
createMerkleRoot hs0 = loop hs0
where
loop hs =
if S.length hs == 1 then
S.index hs 0
else
loop (combine (dupWhenOdd hs))
-- when odd, duplicate last hash
dupWhenOdd hs =
if odd $ S.length hs then
hs |> S.index hs (S.length hs - 1)
else
hs
-- Make newHashes (1/2 size of given hashes)
-- where every element of newHashes is made
-- by taking adjacent pairs of given hashes,
-- concatenating their contents
-- then hashing that concatenated contents.
combine hs = runST $ do
newHashes <- newSTRef S.empty
forM_ (S.chunksOf 2 hs) $ \x ->
modifySTRef' newHashes (|> concatHash (S.index x 0) (S.index x 1))
readSTRef newHashes
nullHash :: HashDigest
nullHash = BS.replicate 32 (chr 0)
t1 :: Spec
t1 =
let one = S.empty |> C.hash "01"
two = one |> C.hash "10"
three = two |> C.hash "11"
in describe "t1" $ do
it "empty" $ createMerkleRoot S.empty `shouldBe` nullHash
it "one" $ createMerkleRoot one `shouldBe` C.hash "01"
it "two" $ createMerkleRoot two `shouldBe` concatHash (C.hash "01") (C.hash "10")
it "three" $ createMerkleRoot three `shouldBe`
"\STX\SYN\n\251\228\227\135\246\SUB\EM\128\196\r\b\166\RS\NAK9\235X\236R\184\134\220\141wP\225\ACK\169\254"
The merkle root, besides acting as a “signature” of the transactions, can be used by lightweight peers to ensure that a transaction is in a block without needing to access (i.e., over a network connection) all the transactions in a block (e.g., Bitcoin’s Simplified Payment Verification).
A lightweight peer contacts a full peer asking for a “merkle path” from a particular transaction, through the merkle tree, to the merkle root.
Assume the lightweight peer has transaction K
. To verify that K
is a member of the block it hashes transaction K
, forming H_K
. It sends H_K
to a full peer. The full peer creates a merkle path by identifying which ordered set of hashes (marked in blue) it needs to send to the lightweight peer. The lightweight peer then uses its K
along with the returned merkle path to hash through that path to reach the root. If resulting hash matches the root then the transaction is verified to be in the block, otherwise not.
The ordered hashes needed by the lightweight peer to hash from its transaction to the root are:
H_L, H_IJ, H_MNOP, H_ABCDEFGH
To create a merkle path an explicit tree may be used:
-- | Left or Right neighbor
type MerklePathElement = Either HashDigest HashDigest
-- | neighbor and parent are `Nothing` for the root
data MerkleInfo = MerkleInfo
{ identity :: ! HashDigest -- ^ for testing
, neighbor :: ! (Maybe MerklePathElement)
, parent :: ! (Maybe HashDigest)
} deriving (Eq, Show)
type MerkleTreeMap = M.Map HashDigest MerkleInfo
-- | This function has the same structure as `createMerkleRoot`.
-- The difference is that this one creates a tree (using a Map).
createMerkleTreeMap :: Hashes -> MerkleTreeMap
createMerkleTreeMap (viewl -> EmptyL) = M.empty
createMerkleTreeMap hs0 = loop (hs0, M.empty)
where
loop (hs, m) =
if S.length hs == 1 then
let h = S.index hs 0
in M.insert h (MerkleInfo h Nothing Nothing) m
else
loop (combine (dupWhenOdd hs) m)
dupWhenOdd hs =
if odd $ S.length hs then
hs |> S.index hs (S.length hs - 1)
else
hs
combine hs m = runST $ do
newHashesAndMap <- newSTRef (S.empty, m)
forM_ (S.chunksOf 2 hs) $ \x -> do
let parentHash = concatHash (S.index x 0) (S.index x 1)
leftHash = S.index x 0
rightHash = S.index x 1
l = MerkleInfo leftHash (Just (Right rightHash)) (Just parentHash)
r = MerkleInfo rightHash (Just (Left leftHash)) (Just parentHash)
modifySTRef' newHashesAndMap (\(hs', m') ->
( hs' |> parentHash
, M.insert leftHash l (M.insert rightHash r m')))
readSTRef newHashesAndMap
Then, given a hash of a transaction and the tree created above, create a path to the root.
type MerklePath = Seq MerklePathElement
merklePathTo :: HashDigest -> MerkleTreeMap -> MerklePath
merklePathTo h m = go (m ! h) S.empty
where
go (MerkleInfo _ _ Nothing) xs = CP.reverse xs
go (MerkleInfo _ (Just (Left l)) (Just p)) xs = go (m ! p) (Left l <| xs)
go (MerkleInfo _ (Just (Right r)) (Just p)) xs = go (m ! p) (Right r <| xs)
go MerkleInfo {} _ = error "merklePathTo"
t2 :: Spec
t2 = do
-- create TXs A .. O
txs' <- runIO (S.replicateM 15 (do rw <- randomWord randomASCII 100; return (BS.pack rw)))
-- create TX hashes H_A .. H_O (see the diagram above)
let hashes = CP.map C.hash txs'
hkData = S.index txs' 10
hK' = C.hash hkData
-- this will dup H_O to add H_P
m = createMerkleTreeMap hashes
-- manually create the pieces needed for the merkle path for H_K
(MerkleInfo _ (Just (Right hL)) (Just hKL)) = m ! hK'
(MerkleInfo _ (Just (Left hIJ)) (Just hIJKL)) = m ! hKL
(MerkleInfo _ (Just (Right hMNOP)) (Just hIJKLMNOP)) = m ! hIJKL
(MerkleInfo _ (Just (Left hABCDEFGH)) (Just hABCDEFGHIJKLMNOP)) = m ! hIJKLMNOP
describe "t2" $ do
it "root" $ createMerkleRoot hashes `shouldBe` hABCDEFGHIJKLMNOP
it "merklePath" $
merklePathTo hK' (createMerkleTreeMap hashes) `shouldBe`
S.empty |> Right hL |> Left hIJ |> Right hMNOP |> Left hABCDEFGH
it "createMerkleTreeMap" $
createMerkleTreeMap (S.empty |> "00" |> "01" |> "02" |> "03")
`shouldBe`
M.fromList [("00",MerkleInfo { identity = "00", neighbor = Just (Right "01")
, parent = Just "\136\139\EM\164;\NAK\SYN\131\200x\149\246!\GS\159\134@\249{\220\142\243/\ETX\219\224W\200\245\229m2"})
,("01",MerkleInfo { identity = "01", neighbor = Just (Left "00")
, parent = Just "\136\139\EM\164;\NAK\SYN\131\200x\149\246!\GS\159\134@\249{\220\142\243/\ETX\219\224W\200\245\229m2"})
,("02",MerkleInfo { identity = "02", neighbor = Just (Right "03")
, parent = Just "\194Wm\216T\SUB\"\\\206\SOHTu\226\213\171\186\201\159${\145DzS\137\130n+\198'@\192"})
,("03",MerkleInfo { identity = "03", neighbor = Just (Left "02")
, parent = Just "\194Wm\216T\SUB\"\\\206\SOHTu\226\213\171\186\201\159${\145DzS\137\130n+\198'@\192"})
,("\136\139\EM\164;\NAK\SYN\131\200x\149\246!\GS\159\134@\249{\220\142\243/\ETX\219\224W\200\245\229m2",MerkleInfo {identity = "\136\139\EM\164;\NAK\SYN\131\200x\149\246!\GS\159\134@\249{\220\142\243/\ETX\219\224W\200\245\229m2", neighbor = Just (Right "\194Wm\216T\SUB\"\\\206\SOHTu\226\213\171\186\201\159${\145DzS\137\130n+\198'@\192"), parent = Just "\156\160c\144$\227\138Z\254|x\231wk\DC3 \228;\235\130:\200\DLE\\0 \131\134w\130\163\243"})
,("\194Wm\216T\SUB\"\\\206\SOHTu\226\213\171\186\201\159${\145DzS\137\130n+\198'@\192",MerkleInfo {identity = "\194Wm\216T\SUB\"\\\206\SOHTu\226\213\171\186\201\159${\145DzS\137\130n+\198'@\192", neighbor = Just (Left "\136\139\EM\164;\NAK\SYN\131\200x\149\246!\GS\159\134@\249{\220\142\243/\ETX\219\224W\200\245\229m2"), parent = Just "\156\160c\144$\227\138Z\254|x\231wk\DC3 \228;\235\130:\200\DLE\\0 \131\134w\130\163\243"})
,("\156\160c\144$\227\138Z\254|x\231wk\DC3 \228;\235\130:\200\DLE\\0 \131\134w\130\163\243",MerkleInfo {identity = "\156\160c\144$\227\138Z\254|x\231wk\DC3 \228;\235\130:\200\DLE\\0 \131\134w\130\163\243", neighbor = Nothing, parent = Nothing})]
Once the light peer receives the merkle path from the full peer it can verify that a transaction is in the block:
isTxInBlock :: Transaction -> MerklePath -> HashDigest -> Bool
isTxInBlock tx mp merkleRoot = loop (C.hash tx) mp == merkleRoot
where
loop h (viewl -> S.EmptyL) = h
loop h (viewl -> (Left x :< xs)) = go x h xs
loop h (viewl -> (Right x :< xs)) = go h x xs
loop _ _ = error "isTxInBlock"
go x y xs = loop (concatHash x y) xs
t3 :: Spec
t3 = do
txs' <- runIO (S.replicateM 15 (do rw <- randomWord randomASCII 100; return (BS.pack rw)))
let hashes = CP.map C.hash txs'
notHKTX = S.index txs' 9
hKTX = S.index txs' 10
hK = C.hash hKTX
merkleRoot = createMerkleRoot hashes
merklePath = merklePathTo hK (createMerkleTreeMap hashes)
describe "t3" $ do
it "hK" $
C.hash (S.index txs' 10) `shouldBe` hK
it "isTxInBlock True" $
isTxInBlock hKTX merklePath merkleRoot `shouldBe` True
it "isTxInBlock False" $
isTxInBlock notHKTX merklePath merkleRoot `shouldBe` False
Diagrams from Mastering Bitcoin by Andreas M. Antonopoulos.
Thanks to Ulises Cerviño Beresi, Victor Cacciari Miraldo, Alejandro Serra Mena and Mark Moir for pre-publication feedback.
The code for this exposition is available at github
run the code: stack test
A discussion is at: reddit
comments powered by Disqus ]]>This post develops a simple blockchain with the goal of understanding the basics of the chain.
setup:
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Chain where
import ClassyPrelude as CP
import Crypto.Hash.SHA256 as C (hash)
import Data.ByteString as BS (concat)
import Data.ByteString.Char8 as BS (pack)
import qualified Prelude as P (tail)
import Data.Sequence as S
import Test.Hspec
The most straigtforward part of a blockchain is the chain itself, a sequence of blocks:
type Blockchain = Seq Block
For the purposes of this exposition, a block in the chain is:
data Block =
Block { bIndex :: ! BIndex -- ^ index of this block in the chain -- for debugging
, bPrevHash :: ! BHash -- ^ hash of previous block
, bTimestamp :: ! BTimestamp -- ^ when this block was created
, bData :: ! BData -- ^ this block's data
} deriving (Eq, Show)
where
type BIndex = Int
type BHash = ByteString
type BTimestamp = ByteString
type BData = ByteString
A hard-coded genesis block:
genesisBlock :: Block
genesisBlock =
let idx = 0
prevHash = "0"
ts = "2017-03-05 10:49:02.084473 PST"
bdata = "GENESIS BLOCK DATA"
in Block idx prevHash ts bdata
begins the chain:
genesisBlockchain :: Blockchain
genesisBlockchain = S.singleton genesisBlock
For this exposition, the only purpose of the genesis block is to provide a verifiable prevHash
for the first “real” block that gets added to the chain.
The chain is tamper-proof because each block contains a hash of of the contents of the previous block:
calculateHash :: BIndex -> BHash -> BTimestamp -> BData -> BHash
calculateHash i p t d = C.hash (BS.concat [BS.pack $ show i, p, t, d])
Since a block contains the hash of the previous block, once a block has been added to a chain, the previous contents of the chain cannot be altered without detection.
For example, using the following functions:
addBlock :: BTimestamp -> BData -> Blockchain -> Blockchain
addBlock ts bd bc = bc |> makeNextBlock bc ts bd
makeNextBlock :: Blockchain -> BTimestamp -> BData -> Block
makeNextBlock bc ts bd =
let (i, ph, _, _) = nextBlockInfo bc ts bd
in Block i ph ts bd
nextBlockInfo :: Blockchain -> BTimestamp -> BData
-> (BIndex, BHash, BTimestamp, BData)
nextBlockInfo bc ts bd =
let prev = getLastCommittedBlock bc
i = bIndex prev + 1
ph = calculateHash (bIndex prev) (bPrevHash prev) (bTimestamp prev) (bData prev)
in (i, ph, ts, bd)
getLastCommittedBlock :: Blockchain -> Block
getLastCommittedBlock bc = S.index bc (S.length bc - 1)
a new block may be added to the chain:
t1 :: Spec
t1 =
let newChain = addBlock "2017-06-11 15:49:02.084473 PST"
"June 11 data"
genesisBlockchain
in describe "t1: add new block to chain" $ do
it "increases length" $
S.length newChain `shouldBe` S.length genesisBlockchain + 1
it "does not change existing blocks" $
getBlock newChain 0 `shouldBe` Just genesisBlock
where
-- | Nothing if index out of range.
getBlock :: Blockchain -> BIndex -> Maybe Block
getBlock bc i = S.lookup i bc
The chained block hashes ensure the integrity of a blockchain. Modification of a parts of any blocks in the chain is detected:
t2 :: Spec
t2 =
let newChain = addBlock "2017-06-12 15:49:02.084473 PST"
"June 12 data"
(addBlock "2017-06-11 15:49:02.084473 PST"
"June 11 data"
genesisBlockchain)
altered1i = (S.index newChain 1) { bIndex = 10 }
badChain1i = S.update 1 altered1i newChain
altered1p = (S.index newChain 1) { bPrevHash = "0" }
badChain1p = S.update 1 altered1p newChain
altered1d = (S.index newChain 1) { bData = "altered June 11 data" }
badChain1d = S.update 1 altered1d newChain
altered12 = (S.index newChain 2) { bData = "altered June 12 data" }
badChain12 = S.update 2 altered12 newChain
in describe "t2: valid blockchain" $ do
it "invalid empty" $
isValidBlockchain S.empty `shouldBe` Left "empty blockchain"
it "valid genesisblockchain" $
isValidBlockchain genesisBlockchain
`shouldBe` Right ()
it "invalid genesisblock" $
isValidBlockchain (S.singleton altered12)
`shouldBe` Left "invalid genesis block"
it "valid newChain" $
isValidBlockchain newChain `shouldBe` Right ()
it "invalid bIndex 1" $
isValidBlockchain badChain1i `shouldBe` Left "invalid bIndex 1"
it "invalid bPrevHash 1" $
isValidBlockchain badChain1p `shouldBe` Left "invalid bPrevHash 1"
it "invalid bPrevHash 2" $
isValidBlockchain badChain1d `shouldBe` Left "invalid bPrevHash 2"
-- This shows that it IS possible to alter the last block in the chain.
-- In a "real" blockchain, consensus and subsequent blocks added to
-- the chain handles this case.
it "changed last element in chain" $
isValidBlockchain badChain12 `shouldBe` Right ()
where
-- | Returns `Just ()` if valid.
-- otherwise `Left reason`
isValidBlockchain :: Blockchain -> Either Text ()
isValidBlockchain bc = do
when (S.length bc == 0) (Left "empty blockchain")
when (S.length bc == 1 && S.index bc 0 /= genesisBlock) (Left "invalid genesis block")
let elements = toList bc
-- `sequence_` causes function to return on/with first `Left` value
sequence_ (CP.map isValidBlock (CP.zip elements (P.tail elements)))
return ()
-- | Given a valid previous block and a block to check.
-- | Returns `Just ()` if valid.
-- otherwise `Left reason`
isValidBlock :: (Block, Block) -> Either Text ()
isValidBlock (validBlock, checkBlock) = do
when (bIndex validBlock + 1 /= bIndex checkBlock) (fail' "invalid bIndex")
when (hashBlock validBlock /= bPrevHash checkBlock) (fail' "invalid bPrevHash")
return ()
where
fail' msg = Left (msg <> " " <> tshow (bIndex validBlock + 1))
hashBlock b = calculateHash (bIndex b) (bPrevHash b) (bTimestamp b) (bData b)
The above is the essence of the chain in blockchain.
Note that there was no discussion of
bData
to hold more than one “transaction”)bData
field in a Block
)A blockchain is a list of blocks, where each block
t3 :: Spec
t3 =
let withOne = addBlock "2017-06-11 15:49:02.084473 PST"
"June 11 data"
genesisBlockchain
withTwo = addBlock "2017-06-12 15:49:02.084473 PST"
"June 12 data"
withOne
in describe "t3: list" $
it "withTwo" $
withTwo `shouldBe`
S.fromList
[ Block { bIndex = 0
, bPrevHash = "0"
, bTimestamp = "2017-03-05 10:49:02.084473 PST"
, bData = "GENESIS BLOCK DATA"
}
, Block { bIndex = 1
, bPrevHash = "'\234-\147\141\"\142\235\CAN \246\158<\159\199s\174\\\225<\174\188O\150oM\217\DC3'\237\DC4n"
, bTimestamp = "2017-06-11 15:49:02.084473 PST"
, bData = "June 11 data"
}
, Block { bIndex = 2
, bPrevHash = "\145\238k24\175\147I\EOT\208\204\210\190s\192<b:\SOH\215\DC1\254)\173\EOT\186\220\US\SYNf\191\149"
, bTimestamp = "2017-06-12 15:49:02.084473 PST"
, bData = "June 12 data"
}
]
Thanks to Ulises Cerviño Beresi, Heath Matlock, Alejandro Serra Mena and Mark Moir for pre-publication feedback.
The code for this exposition is available at : github
run the code: stack test
A discussion is at: reddit
comments powered by Disqus ]]>Slides for my May 27, 2017 LambdaConf talk on recursion schemes.
This presentation is unique in that shows “list only” versions of a number of recursions schemes. This makes it easier to understand for beginners (because it avoids the need to talk about Fix
).
I gave a talk at Lambda Lounge Utah on “Programming and Math”.
I gave a slightly different version of this talk at LambdaConf a couple of months earler.
comments powered by Disqus ]]>Over the years I occasionally reimplemented my rdf-triple-browser, first in GWT, then Java/Swing and then last summer in Haskell using Threepenny-gui (which I will call ‘TPG’ for the rest of this article). The hardest part for me was understanding how to connect to external events.
The documentation says newEvent :: IO (Event a, Handler a)
“Create a new event. Also returns a function that triggers an event occurrence.” What is doesn’t say it that calling the returned Handler
causes the specific returned Event
to happen.
I got lost in the documentation about Handler
, going off on dead ends with register
. Even the TAs at last summer’s Utrecht Haskell Summer School could not figure it out (although, to be fair, they did not spend that much time on it, they were concentrating on course questions).
Fortunately, Max Taldykin, on stackoverflow, provided a small program that lead me to discover the “magic” of how to use what is returned via newEvent
. Below I show an even smaller program that shows how it is wired.
1 module ThreepennyExternalNewEventDemo where
2
3 import Control.Concurrent (forkIO)
4 import Graphics.UI.Threepenny
5 import Network
6 import System.IO (hClose)
7
8 main :: IO ()
9 main = do
10 (eAccept, hAccept) <- newEvent
11 forkIO (acceptLoop hAccept 6789)
12 forkIO (acceptLoop hAccept 9876)
13 startGUI defaultConfig $ \win -> do
14 bAccept <- stepper "" eAccept
15 entree <- entry bAccept
16 element entree # set (attr "size") "10" # set style [("width","200px")]
17 getBody win #+ [element entree]
18 return ()
19
20 acceptLoop :: (String -> IO a) -> PortNumber -> IO b
21 acceptLoop hAccept bindAddr = do
22 s <- listenOn $ PortNumber bindAddr
23 loop s
24 where
25 loop s = do
26 (h, hostname, portNumber) <- accept s
27 hClose h
28 hAccept $ show bindAddr ++ " " ++ hostname ++ " " ++ show portNumber
29 loop s
Line 10 uses newEvent
to create an Event a
and Handler a
. The “magic” is that behind the scenes TPG has wired the return Handler
such that when it is called it generates an Event
specific to the returned Event
.
Line 14 creates a Behavior
from that specific Event
. That Behavior
is then given to the entry
widget. Whenever the code calls the Handler
it triggers that Event
that causes the Behavior
to change. The widget shows the Behavior
change.
Here is the wiring for the program:
Line 11 starts two threads that accept connection at two different listening ports. Whenever a connection is accepted, the loops call the given Handler
at line 28. The Handler
“magically” triggers an Event
. The Event
is turned into a Behavior
via stepper
. The Behavior
was previously wired to the entry
widget. So, whenever a connection is accepted the text field shows the info. That’s it!
Hopefully this small example will save someone some time.
comments powered by Disqus ]]>My talk at Java one on InfiniBand communication infrastructure.
comments powered by Disqus ]]>Erik Meijer points out a typo (or conceptual error) in an FPComplete article that calls JSON a protocol and HTTP a format.
The PEPT remoting architecture considers:
a transport to be something that moves bits from one location to another
a transport is something where the program does not manipulate header bits
e.g., most apps do not touch TCP/IP header bits, they just write/read from the TCP/IP streams
a remoting protocol is something where, besides moving bits, the program will manipulate the header bits
e.g., HTTP is a protocol for REST
e.g., HTTP is a transport for SOAP (in general)
a remoting format is
a serial encoding of the application data
a serial encoding of protocol and/or transport headers
e.g., JSON is a common application format for REST
e.g., XML is both the application format and the protocol format for SOAP
e.g., CDR is both the application format and the protocol format for CORBA IIOP
There are, of course, special cases, but the above taxonomy provides a useful separation of concerns.
comments powered by Disqus ]]>One of the tools I use for drawing graphs is dot
from Graphviz. Recently I was drawing a series of diagrams that have mostly identical parts. It was error-prone and a pain to keep the identical parts in sync between the multiple *.dot
files. I found Haskell’s Data.GraphViz package to be the solution.
The documentation for that package is great, but it needs a few more examples. The purpose of this post is to add examples to the pool.
This post uses the following extension and imports:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Data.Graph.Inductive
import Data.GraphViz
import Data.GraphViz.Attributes.Complete
import Data.GraphViz.Types.Generalised as G
import Data.GraphViz.Types.Monadic
import Data.Text.Lazy as L
import Data.Word
import WriteRunDot
When searching for Data.GraphViz
examples one of the most useful I found was
http://speely.wordpress.com/2010/09/17/haskell-graphs-and-underpants/
https://github.com/mcandre/mcandre/blob/master/haskell/gnomes.hs — more up-to-date
It shows how to turn Data.Graph.Inductive graphs into dot graphs. The input:
ex1 :: Gr Text Text
ex1 = mkGraph [ (1,"one")
, (3,"three")
]
[ (1,3,"edge label") ]
ex1Params :: GraphvizParams n L.Text L.Text () L.Text
ex1Params = nonClusteredParams { globalAttributes = ga
, fmtNode = fn
, fmtEdge = fe
}
where fn (_,l) = [textLabel l]
fe (_,_,l) = [textLabel l]
ga = [ GraphAttrs [ RankDir FromLeft
, BgColor [toWColor White]
]
, NodeAttrs [ shape BoxShape
, FillColor (myColorCL 2)
, style filled
]
]
along with some color helper functions:
-- http://www.colorcombos.com/color-schemes/2025/ColorCombo2025.html
myColorCL :: Word8 -> ColorList
myColorCL n | n == 1 = c $ (RGB 127 108 138)
| n == 2 = c $ (RGB 175 177 112)
| n == 3 = c $ (RGB 226 206 179)
| n == 4 = c $ (RGB 172 126 100)
where c rgb = toColorList [rgb]
myColor :: Word8 -> Attribute
myColor n = Color $ myColorCL n
results in:
Besides supporting Data.Graph.Inductive
, Data.GraphViz
provides several Haskell representations: canonical, generalized, graph and monadic. The monadic
representation looks very similar to dot notation:
ex2 :: G.DotGraph L.Text
ex2 = digraph (Str "ex2") $ do
graphAttrs [RankDir FromLeft]
nodeAttrs [style filled]
cluster (Int 0) $ do
node "Ready" [ textLabel "ready"
, shape DoubleCircle, myColor 1, FixedSize True, Width 1]
cluster (Int 1) $ do
graphAttrs [textLabel "active"]
node "Open" [ textLabel "open"
, shape Circle, myColor 2, FixedSize True, Width 1]
node "OpenExpectFragment" [ textLabel "open expect\nfragment"
, shape Circle, myColor 2, FixedSize True, Width 1]
node "HalfClosed" [ textLabel "half-clsd"
, shape Circle, myColor 2, FixedSize True, Width 1]
node "endMessage?" [ textLabel "end req?"
, shape DiamondShape, myColor 4, FixedSize True, Width 1.25, Height 1.25]
node "fragmentEndMessage?" [ textLabel "end req?"
, shape DiamondShape, myColor 4, FixedSize True, Width 1.25, Height 1.25]
node "requestFragment" [ textLabel "FRAGMENT"
, shape BoxShape, myColor 3]
"Open" --> "endMessage?"
edge "endMessage?" "HalfClosed" [textLabel "true"]
edge "endMessage?" "OpenExpectFragment" [textLabel "false"]
"OpenExpectFragment" --> "requestFragment"
"requestFragment" --> "fragmentEndMessage?"
edge "fragmentEndMessage?" "OpenExpectFragment" [textLabel "false"]
edge "fragmentEndMessage?" "HalfClosed" [textLabel "true"]
cluster (Int 2) $ do
graphAttrs [textLabel "done"]
node "Closed" [ textLabel "closed"
, shape DoubleCircle, myColor 1, FixedSize True, Width 1]
-- outside the box(es)
node "request" [ textLabel "REQUEST"
, shape BoxShape, myColor 3]
node "response" [ textLabel "RESPONSE"
, shape BoxShape, myColor 3]
"Ready" --> "request"
"request" --> "Open"
"HalfClosed" --> "response"
"response" --> "Closed"
The above results in (a diagram for the beginnings of a simple wire protocol with possibly fragmented request messages and single response messages):
Quite often I create diagrams that do not use clustering but have different node types, each type with a distinct shape, size and color. In dot, one can factor the shared attributes via subgraph
:
digraph ex3 {
graph [rankdir=LR];
subgraph {
node [shape=doublecircle,fixedsize=true,width=1,style=filled,color="#7f6c8a"];
Open [label=open];
Closed [label=closed];
}
subgraph {
node [shape=circle,fixedsize=true,width=1,style=filled,color="#7f6c8a"];
ClosedWaitingAck [label="clsd waiting\nACK"];
}
subgraph {
node [shape=box,width=1,style=filled,color="#e2ceb3"];
cancel [label=CANCEL];
cancelAck [label=CANCEL_ACK];
}
Open -> cancel;
cancel -> ClosedWaitingAck;
ClosedWaitingAck -> cancelAck;
cancelAck -> Closed;
}
which results in:
Data.GraphViz
supports subgraph
for Data.Graph.Inductive
graphs via the isDotCluster
setting in GraphvizParams
.
It also supports subgraph
for Data.GraphViz.Types
Canonical
and Generalised
graphs via setting isCluster
to False
for their appropriate DotSubGraph
types.
However, Graph
and Monadic
do not (yet) have a setting that supports subgraph
.
The dot output above was produced from:
ex3 :: G.DotGraph L.Text
ex3 = digraph (Str "ex3") $ do
graphAttrs [RankDir FromLeft]
cluster (Int 0) $ do
nodeAttrs [shape DoubleCircle, FixedSize True, Width 1, style filled, myColor 1]
node "Open" [textLabel "open"]
node "Closed" [textLabel "closed"]
cluster (Int 1) $ do
nodeAttrs [shape Circle, FixedSize True, Width 1, style filled, myColor 1]
node "ClosedWaitingAck" [textLabel "clsd waiting\nACK"]
cluster (Int 2) $ do
nodeAttrs [shape BoxShape, Width 1, style filled, myColor 3]
node "cancel" [textLabel "CANCEL"]
node "cancelAck" [textLabel "CANCEL_ACK"]
"Open" --> "cancel"
"cancel" --> "ClosedWaitingAck"
"ClosedWaitingAck" --> "cancelAck"
"cancelAck" --> "Closed"
which almost has what I want as output:
digraph exe {
graph [rankdir=LR];
subgraph cluster_0 {
...
Manually removing the cluster_N
after subgraph
gives me what I want.
If cluster_N
is not removed what results is:
which is not what I’m after.
Since all of Haskell is available to build the graph, one can do:
doubleCircle :: n -> Text -> Dot n
doubleCircle n l = node n [textLabel l, shape DoubleCircle, FixedSize True, Width 1, style filled, myColor 1]
circle :: n -> Text -> Dot n
circle n l = node n [textLabel l, shape Circle, FixedSize True, Width 1, style filled, myColor 1]
rectangle :: n -> Text -> Dot n
rectangle n l = node n [textLabel l, shape BoxShape, Width 1, style filled, myColor 3]
open, closed, waiting, cancel, cancelAck :: Dot L.Text
open = doubleCircle "Open" "open"
closed = doubleCircle "Closed" "closed"
waiting = circle "ClosedWaitingAck" "clsd waiting\nACK"
cancel = rectangle "cancel" "CANCEL"
cancelAck = rectangle "cancelAck" "CANCEL_ACK"
ex4 :: G.DotGraph L.Text
ex4 = digraph (Str "ex4") $ do
graphAttrs [RankDir FromLeft]
open; closed; waiting; cancel; cancelAck
"Open" --> "cancel"
"cancel" --> "ClosedWaitingAck"
"ClosedWaitingAck" --> "cancelAck"
"cancelAck" --> "Closed"
This results in the exact same output as the manually editted graph:
I also use the above “trick” to factor out common parts of graphs and share them (my original motivation for using Data.GraphViz
instead of manually writing dot
).
Images for these examples can be created using the utilities (the important piece being runGraphvizCommand
and addExtension
):
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
module WriteRunDot where
import Control.Monad (forM_)
import Data.GraphViz
import System.FilePath
doDots :: PrintDotRepr dg n => [(FilePath, dg n)] -> IO ()
doDots cases = forM_ cases createImage
createImage :: PrintDotRepr dg n => (FilePath, dg n) -> IO FilePath
createImage (n, g) = createImageInDir "/tmp" n Png g
createImageInDir :: PrintDotRepr dg n => FilePath -> FilePath -> GraphvizOutput -> dg n -> IO FilePath
createImageInDir d n o g = Data.GraphViz.addExtension (runGraphvizCommand Dot g) o (combine d n)
and use the utilities via:
main :: IO ()
main = do
doDots [ ("ex1" , graphToDot ex1Params ex1) ]
doDots [ ("ex2" , ex2)
, ("ex3" , ex3)
, ("ex4" , ex4)
]
Using Data.GraphViz
I can now write dot diagrams but use Haskell to factor out the common parts of similar diagrams (not shown in the examples above). Of course, I also have the full power of Haskell available. And, when using a interactive Haskell environment (see Tim Dysinger’s emacs environment), the IDE catches type errors, syntax errors, duplicates, etc., while your write. A great improvement over manually writing and maintaining *.dot
files.
The emacs org-mode literate source code of this article is available at:
comments powered by Disqus ]]>For writing articles on Haskell, rather than showing ghci
input/output like:
Prelude> map (*2) [1..10]
[2,4,6,8,10,12,14,16,18,20]
I do the following:
import Test.HUnit
import Test.HUnit.Util -- https://github.com/haroldcarr/test-hunit-util
…
t1 = t "t1"
(map (*2) [1..10]) -- "input"
[2,4,6,8,10,12,14,16,18,20] -- "output"
… or, if many examples evaluate to same value:
t2 = tt "t2"
[(map (*2) [1..10]) -- "input1"
,(map (\x -> x * 2) [1..10]) -- "input2"
]
[2,4,6,8,10,12,14,16,18,20] -- "output"
Then, in this section at the end of the article, I show the test setup:
main = do
runTestTT $ TestList $ t1 ++ t2
and its evaluation:
main
=> Counts {cases = 3, tried = 3, errors = 0, failures = 0}
Also note, that when I do actually show ghci
input/output, rather than do:
*Main> :t t1
t1 :: [Test]
I do:
:t t1
=> t1 :: [Test]
The code for t
, tt
and a couple of other short aliases is at
My presentation at JavaOne 2013.
ABSTRACT
The JAX-WS standard includes APIs for using POJOs or XML for remote messages. But it does not include APIs for letting the user control the transport. This BoF discusses adding pluggable transport APIs to the JAX-WS standard.
This BoF shows a candidate pluggable transport mechanism for JAX-WS that enables one to use other transports besides HTTP. In particular, it shows the benefits of using WebSockets and InfiniBand transports for SOAP message exchanges.
SUMMARY
Background:
Besides being able to send and receive remote method calls with Java objects (POJOs), JAX-WS has APIs that enable one to operate at the XML level. “Dispatch” is available on the client-side to feed XML into JAX-WS for soap-processing (e.g., handling security). The message is then sent using the transport built into the system. After the response is received and processed, the XML is given back to Dispatch. Dispatch also has an asynchronous mode.
The service-side has a similar “Provider” API, except Provider does not have an asynchronous mode. However, the JAX-WS specification does not have corresponding APIs for giving the user control of the transport in a similar manner.
Pluggable Transports:
Asynchronous APIs for client and service transport are shown, as well as APIs for plugging those transports into the underlying JAX-WS system. Using those APIs, the BoF includes a demonstration of plugging in WebSocket, InfiniBand and RDMA transports into an existing system.
The benefits of these transports are shown, such as:
The benefits are not without drawbacks, such as:
These APIs have been used in a production environment.
Harold Carr
architect of SOAP Web Services Technology at Oracle
Last Modified : 2013 Dec 15 (Sun) 13:15:19 by carr.
THE FOLLOWING IS INTENDED TO OUTLINE OUR GENERAL PRODUCT DIRECTION. IT IS INTENDED FOR INFORMATION PURPOSES ONLY, AND MAY NOT BE INCORPORATED INTO ANY CONTRACT. IT IS NOT A COMMITMENT TO DELIVER ANY MATERIAL, CODE, OR FUNCTIONALITY, AND SHOULD NOT BE RELIED UPON IN MAKING PURCHASING DECISIONS. THE DEVELOPMENT, RELEASE, AND TIMING OF ANY FEATURES OR FUNCTIONALITY DESCRIBED FOR ORACLE’S PRODUCTS REMAINS AT THE SOLE DISCRETION OF ORACLE.
client APIs to enter/exit JAX-WS
for request processing
DispatcherRequest
; exit: ClientRequestTransport
for response processing
ClientResponseTransport
; exit: DispatcherResponse
service APIs to enter/exit JAX-WS
for request processing
ServiceRequestTransport
; exit: ProviderRequest
for response processing
ProviderResponse
; exit: ServiceResponseTransport
example dual HTTP/WebSocket transport (uses JSR-356)
potential InfiniBand transport
you will not learn WebSocket nor InfiniBand in detail
JAX-WS standard
includes APIs for
POJO : @WebServiceRef
, @WebService
, …
XML : Dispatch
, Provider
, …
async : Dispatch.invokeAsyc
does not include APIs
user transport : WebSockets
, InfiniBand
, …
async everywhere : async Provider
, …
thread guarantees
propose
adding pluggable async transport APIs
adding async Dispatch
and Provider
thread guarantees
ability to use alternate transports
e.g., not supplied by vendor/implementation
performance
use “last minute” binary encodings
long lasting connections
Drawbacks
connection management responsibility of transport impl instead of platform
memory management for zero-copy
@WebService
public class Hello {
@Resource
protected WebServiceContext context;
@WebMethod
public String hello(String name) { ... }
}
public class HelloClient {
@WebServiceRef(wsdlLocation="...?wsdl")
HelloService service;
...
final Hello port = service.getHelloPort();
((BindingProvider)port).getRequest/ResponseContext();
final String response = port.hello(av[0]);
PROS
easy to use/deploy
client port
/ BindingProvider
access to request/response context
service access to request context
CONS
client context.put
sticky : not per-request
no access to response context on service side
entire XML marshaled to/from POJOs
not async
no access to XML
no thread guarantees
cannot “own” transport
Dispatch
/ Provider
example@ServiceMode(value=Service.Mode.PAYLOAD) // MESSAGE
@WebServiceProvider(serviceName="HelloService", portName="HelloPort", ...)
public class HelloImpl implements Provider<Source> {
@Resource
protected WebServiceContext context;
public Source invoke(Source source) { ... } }
public class HelloClient {
@WebServiceRef(wsdlLocation="...?wsdl")
HelloService service;
...
Dispatch<Source> d = // SOAPMessage
service.createDispatch(portQName, Source.class,
Service.Mode.PAYLOAD);
Map<String, Object> c = d.getRequest/ResponseContext();
Source result = d.invoke(...); // .invokeAsync
Dispatch
/ Provider
PROS/CONSPROS
access to XML
Dispatch
has async
access to context
CONS
no Provider
async
no response context access in Provider
Dispatch
/ BindingProvider
response context decoupled from AsyncHandler
no thread guarantees
cannot “own” transport
only Source
and SOAPMessage
supported
InputStream
toodemo
Using JSR-356 : JavaAPI for WebSocket
<wsdl:definitions ...> ...
<wsdl:binding name="MyBinding" type="MyPortType"> ...
<soap12:binding
transport="http://schemas.microsoft.com/soap/websocket"/>
<wsdl:operation name="MyOp"> ... </wsdl:operation>
</wsdl:binding>
<wsdl:service name="MyService">
<wsdl:port name="MyPort" binding="MyBinding">
<soap12:address location=" ws://myHost/myService/" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
GET http://myHost/myService HTTP/1.1
Connection: Upgrade,Keep-Alive
Upgrade: websocket
Sec-WebSocket-Key: ROOw9dYOJkStW2nx5r1k9w==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: soap
soap-content-type: application/soap+msbinsession1
microsoft-binary-transfer-mode: Buffered
Accept-Encoding: gzip, deflate
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: soap
demo
Using proprietary JavaAPIs on top of proprietary C APIs.
InfiniBand slides taken from
Vadim Makhervaks (Oracle)
http://www.ics.uci.edu/~ccgrid11/files/ccgrid11-ib-hse_last.pdf
MessageContext
/ Factory
public interface MessageContextFactory {
MessageContext createContext();
MessageContext createContext(SOAPMessage m);
MessageContext createContext(Source m);
MessageContext createContext(Source m,O EnvelopeStyle.Style envelopeStyle);
MessageContext createContext(InputStream in,
String contentType) throws IOException;
}
public interface MessageContext {
SOAPMessage getAsSOAPMessage() throws SOAPException;
SOAPMessage getAsSource();
ContentType writeTo(OutputStream out) throws IOException;
ContentType getContentType();
Object get(Object k);
void put(Object k, Object v);
}
table of contents (non-functional)
MessageContext
/ Factory
I have a large digitized music collection, primarily encoded in lossless FLAC. Great for listening at home. But lossy MP3 is best for mobile devices, in terms of size and the ability of players to handle encoding formats.
So I wrote a script to make parallel MP3 tree of my canonical music collection (which also includes FLAC, WAV, AIFF, MP3, …).
I wrote it in Haskell, with the help of the Shelly shell scripting DSL and ffmpeg
.
I am not an experienced Haskell/Shelly programmer. If you have suggestions for improvements please let me know (including useful ffmpeg
settings).
http://hackage.haskell.org/packages/archive/shelly/1.3.0.7/doc/html/Shelly.html
https://github.com/yesodweb/Shelly.hs/blob/master/doc/shell-typed.markdown
https://github.com/yesodweb/Shelly.hs/blob/master/src/Shelly.hs
http://stackoverflow.com/questions/18187944/haskell-shelly-sample-code
Other haskell shell examples: http://www.haskell.org/haskellwiki/Applications_and_libraries/Operating_system#Haskell_shell_examples
sudo cabal install shelly shelly-extra
sudo port search ffmpeg
sudo port install ffmpeg @1.2.2
ffmpeg -i "/Volumes/my-music/Ahmad Jamal/The Awakening/01-The Awakening.flac" -qscale:a 0 "/tmp/JUNK.mp3"
https://github.com/haroldcarr/make-mp3-copies
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
import Control.Applicative
import Control.Exception (bracket, handle, SomeException)
import Control.Monad
import Control.Monad.IO.Class
import Data.Maybe (fromJust)
import qualified Data.Text as T
import Shelly
import System.Directory
import System.FilePath
import System.IO (IOMode(..), hClose, hFileSize, openFile)
default (T.Text)
fromRoot = "."
toRoot = "/tmp/JUNK/"
main = shelly $ verbosely $ do
processDir fromRoot
processDir fromPath = do
contents <- ls fromPath
forM_ contents $ \from -> do
isDir <- test_d from
if isDir
then do { maybeCreateDir from; processDir from}
else processFile from $ takeExtension $ fpToString from
maybeCreateDir from = do
let to = mkToFilePath from
dirExists <- test_d to
if dirExists
then say "DIR EXISTS" to
else do { mkdir_p to; say "DIR CREATED" to }
processFile from ".flac" = maybeDo convert True False from
processFile from ".mp3" = maybeDo copy False True from
processFile from ".jpg" = maybeDo copy False True from
processFile from _ = say "IGNORED" from
maybeDo f extP sizeP from = do
let to = mkToFilePath $ if extP then (fromText (T.pack (replaceExtension (fpToString from) ".mp3"))) else from
fileExists <- test_f to
if fileExists
then doIf f sizeP from to
else f from to
doIf f sizeP from to = do
fromSize <- lio getFileSize from
fromTime <- lio getModificationTime from
toSize <- lio getFileSize to
toTime <- lio getModificationTime to
if fromTime > toTime || (sizeP && (fromJust fromSize) /= (fromJust toSize))
then f from to
else say "FILE EXISTS" to
convert from to = do
flacToMp3 (toTextIgnore from) (toTextIgnore to)
say "FILE CONVERTED" to
where
flacToMp3 from to = run_ "ffmpeg" ["-i", from, "-qscale:a", "0", to]
copy from to = do
cp from to
say "FILE COPIED" to
mkToFilePath path =
(fpToString toRoot) Shelly.</> (fpToString path)
fpToString fp = T.unpack $ toTextIgnore fp
say msg fp =
liftIO $ putStrLn $ show (fpToString fp) ++ " " ++ msg
lio f fp =
liftIO . f $ fpToString fp
-- from Real World Haskell
getFileSize path = handle handler $
bracket (openFile path ReadMode) (hClose) (\h -> do
size <- hFileSize h
return $ Just size)
where
handler :: SomeException -> IO (Maybe Integer)
handler _ = return Nothing
-- End of file.
In the present version, cd
to the root of the canonical music collection and run the script. The output location is hardwired in the code.
export MP3=~/make-mp3-copies
alias m3='$MP3/MakeMP3Copies'
cd $MP3
ghc MakeMP3Copies.hs
export PATH=.:$PATH
pushd "/Volumes/my-music/Ahmad Jamal/"
m3
comments powered by Disqus
]]>