These are my second impressions of the Agda programming language after writing (or trying to write) a few simple Agda programs.
If you only want the highlights, just read the Executive Summary. If you also want to see supporting material and notes, continue reading further.
Agda is simultaneously touted as (1) a dependently-typed programming language and (2) a proof assistant. I find that Agda is a considerably better proof assistant than it is a programming language at this time.
A few issues I found when trying to use Agda as a programming language to get real work done:
IO
module. Yet another abstract academic construct.
I can work around this by restricting myself to the IO.Primitive
module. This module directly mirrors Haskell’s IO system and thus only requires knowledge of monads.
div
and mod
operators for the default implementation of natural numbers (Data.Nat
) run in linear time relative to the magnitude of the divisor.1
At least ≤?
, ≟
, +
, and *
appear to be logarithmic- or constant-time.
The “Hello World” program compiles to a 14 MB binary. Unacceptably large.
The “Hello World” program takes 3 seconds to compile. Unacceptably slow.
The following are all the simple programs I’ve written with Agda so far.
A manual reimplementation of natural numbers (i.e. non-negative integers) just so that I can get my Agda environment up and running.3
module Peano where
data ℕ : Set where
zero : ℕ
suc : ℕ → ℕ
_+_ : ℕ → ℕ → ℕ
zero + zero = zero
zero + n = n
(suc n) + n' = suc (n + n')
A program that prints the number 5.
No more reimplementing silly stuff like numbers. Let’s use the standard library.
module HelloNumber where
-- Agda standard library 0.7
open import Data.Nat
open import Data.Nat.Show using (show)
open import Data.Unit using (⊤)
open import IO
import IO.Primitive as Prim
five : ℕ
five = 5
main' : IO ⊤
main' = putStrLn (show five)
main : Prim.IO ⊤
main = run main'
IO ⊤
(which would be equivalent to the Haskell IO ()
). Instead it has type Prim.IO ⊤
which can be obtained by passing an IO ⊤
through the IO.run
function.Now let’s try printing strings, which should allow writing the standard “Hello world” program.
module HelloWorld where
-- Agda standard library 0.7
open import Data.String
open import Data.Unit using (⊤)
open import IO
import IO.Primitive as Prim
greeting : String
greeting = "Hello World"
main' : IO ⊤
main' = putStrLn greeting
main : Prim.IO ⊤
main = run main'
Now let’s try stringing multiple IO actions together.
module PrintTwoThings where
-- Agda standard library 0.7
open import Coinduction using (♯_)
open import Data.String
open import Data.Unit using (⊤)
open import IO
import IO.Primitive as Prim
doThis : IO ⊤
doThis = putStr "Everybody say coinductive deep embedding! "
thenThat : IO ⊤
thenThat = putStrLn "Coinductive deep embedding!"
main' : IO ⊤
main' = (♯ doThis) >> (♯ thenThat)
main : Prim.IO ⊤
main = run main'
>>
operator used to join two IO actions isn’t IO A -> IO B -> IO B
. Rather it is ∞ (IO A) -> ∞ (IO B) -> IO B
.
∞
type before I can chain them together.∞
type? No idea. It has something to do with “coinduction”, which Wikipedia failed to explain to me coherently.
∞ A
represents a lazy unevaluated computation of type A
that might not terminate when it is actually evaluated.Coinduction
library, I can make a ∞
using the ♯
operator which takes a value of any type A
and makes a ∞ A
out of it.action1 >> action2
I need ♯ action1 >> ♯ action2
Read a line from standard input. Write it back to the screen.
module EchoInput where
-- Agda standard library 0.7
open import Data.String
open import Foreign.Haskell using (Unit)
open import IO.Primitive
postulate
getLine : IO Costring
{-# COMPILED getLine getLine #-}
doThis : IO Costring
doThis = getLine
thenThat : Costring → IO Unit
thenThat = λ s → putStrLn s
main : IO Unit
main = doThis >>= thenThat
stdin
itself. Unbelievable.
Primitive.IO
module instead of the regular IO
module. I no longer need to use ∞
or ♯
either, which is nice.module PromptForName where
-- Agda standard library 0.7
open import Data.String
open import Foreign.Haskell using (Unit)
open import IO.Primitive
postulate
getLine : IO Costring
{-# COMPILED getLine getLine #-}
main : IO Unit
main =
putStrLn (toCostring "What's your name?") >>= (λ _ →
getLine >>= (λ s →
putStr (toCostring "Hello ") >>= (λ _ →
putStrLn s)))
Primitive.IO
defines the >>=
operator but not >>
. Asymmetric with the regular IO
module.Costring
objects. So a string literal has to be passed to toCostring
to be used.Read a line from standard input. Reverse it in Agda. Write the reversed line back to the screen.
module EchoInputReverse where
-- Agda standard library 0.7
open import Data.List using (reverse)
open import Data.String
open import Foreign.Haskell using (Unit)
open import IO.Primitive
postulate
getLine : IO String
{-# COMPILED getLine getLine #-}
main : IO Unit
main =
getLine >>= (λ s →
return (toCostring (fromList (reverse (toList s)))) >>= (λ s' →
putStrLn s'))
Costring
. I can’t reverse it or do anything useful with it without first converting it to a String
. But there is no function in the standard library to do this conversion, even as a runtime cast.String
maps to the Agda type Costring
in the general case, it is still possible to postulate that it maps directly to the Agda type String
for certain functions where you can assert that the string won’t be infinite.
Costring
and String
datatypes. This is not documented.getLine
as being a finite string when it may not in fact be. See discussion at StackOverflow.Prompt the user for a number, parse it, and output it. If an invalid number was input, output zero.
module ParseInt where
-- Agda standard library 0.7
open import Data.Char
open import Data.List
open import Data.Maybe
open import Data.Nat
open import Data.Nat.Show
open import Data.String
open import Foreign.Haskell using (Unit)
open import IO.Primitive
postulate
getLine : IO String
{-# COMPILED getLine getLine #-}
parseInt : String → Maybe ℕ
parseInt s =
then? (unwrap (parseDigits s)) (λ s' →
just (digitsToℕ s'))
where
parseDigits : String → List (Maybe ℕ)
parseDigits s = map toDigit (toList s) where
toDigit : Char → Maybe ℕ
toDigit '0' = just 0
toDigit '1' = just 1
toDigit '2' = just 2
toDigit '3' = just 3
toDigit '4' = just 4
toDigit '5' = just 5
toDigit '6' = just 6
toDigit '7' = just 7
toDigit '8' = just 8
toDigit '9' = just 9
toDigit _ = nothing
-- TODO: There's probably a standard-library function that does this
-- but I can't find it.
unwrap : List (Maybe ℕ) → Maybe (List ℕ)
unwrap xs = unwrap' (just []) xs where
unwrap' : Maybe (List ℕ) → List (Maybe ℕ) → Maybe (List ℕ)
unwrap' (just xs) (just y ∷ ys) = unwrap' (just (Data.List._++_ xs [ y ])) ys -- makes unwrap O(N^2)!
unwrap' (just xs) (nothing ∷ _) = nothing
unwrap' (just xs) [] = just xs
unwrap' nothing _ = nothing
-- TODO: Look for a monadic _>>=_ that does the same thing
then? : {A : Set} → {B : Set} → Maybe A → (A → Maybe B) → Maybe B
then? nothing _ = nothing
then? (just r1) op2 = op2 r1
digitsToℕ : List ℕ → ℕ
digitsToℕ xs = digitsToℕ' (reverse xs) where
digitsToℕ' : List ℕ → ℕ
digitsToℕ' [] = 0
digitsToℕ' (x ∷ xs) = x + (10 * (digitsToℕ' xs))
ℕ? : Maybe ℕ -> ℕ
ℕ? (just x) = x
ℕ? nothing = 0
main : IO Unit
main =
getLine >>= (λ s →
return (show (ℕ? (parseInt s))) >>= (λ s' →
putStrLn (toCostring s')))
Actual:
List Char !=< _A_10 s → _B_11 s of type Set
when checking that the expression toList s has type
_A_10 s → _B_11 s
Better:
Expected type (A → B) but found type (List Char).
show
function that converts a natural number to a string has quadratic performance!
+
, *
, mod
} is not constant time. Not cool.
Indeed the _divMod_
operator runs in linear time with respect to the magnitude of the dividend. Madness.
=
or ==
but rather ≟
.
Bool
either. Instead it returns a fancy boolean called a Decidable
with an embedded equality proof.Decidable
can be converted to a regular boolean with the ⌊_⌋
operator. Weird.Data.Nat
take linear memory to represent rather than logarithmic.
Data.Bin
which represents natural numbers using binary, but most of the standard library doesn’t use it.There is another module called Data.Bin
which provides a representation of natural numbers which uses an efficient binary representation. But most of the standard library does not use it.↩
I found configuring Emacs for Agda to be nontrivial. I had to learn enough Emacs Lisp to debug my .emacs
configuration file.↩
Kudos to Learn You an Agda > Hello, Peano for helping me implement this first Agda program.↩