Skip to content

Commit

Permalink
added comments for haskell newbies
Browse files Browse the repository at this point in the history
  • Loading branch information
Arnaud Bailly authored and Arnaud Bailly committed Feb 23, 2012
1 parent 2376b82 commit b4a1521
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 23 deletions.
47 changes: 45 additions & 2 deletions Music.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,33 @@ import Sound
type Octave = Int
type Tempo = Int

-- twelve half-tones form a chromatic scale
-- | @data@ keyword introduces a new concrete data-type and its set of
-- constructors. Here we define a simple enumerated type consisting in
-- the twelve notes of the chromatic scale. Note that the lexer/parser
-- of Haskell imposes the following restrictions on identifiers:
--
-- * Types and Constructors identifiers must start with an upper-case letter
-- * Functions and variables identifiers must start with a lower-case letter
-- * Types and Constructor operators must start with a colon (:)
data Pitch = C | Cs |
D | Ds |
E |
F | Fs |
G | Gs |
A | As |
B
-- a deriving clause provides automatic derivation of the functions
-- defined in the corresponding type-class. This derivation is by
-- default restricted to a predefined set of standard type classes
-- provided in the Prelude (or the compiler) but it can be extended
-- using some language extensions.
deriving (Eq,Ord,Show,Read,Enum)

-- note values (in british notation)
-- | Note values (in british notation).
-- Here we use a recursive constructor for @Pointed Duration@. Technically this
-- kind of objects are called *products* as they represent the cartesian product
-- of the possible values of the composed types (sums are simply the enumeration
-- of all the constructors).
data Duration = Pointed Duration |
Semiquaver |
Quaver |
Expand All @@ -24,15 +40,39 @@ data Duration = Pointed Duration |
Breve
deriving (Eq,Ord,Show,Read)


-- | A Note defined using records-notation.
-- A Haskell record is similar to a product-type but provides syntactic sugar to
-- introduce accessors for components (eg. attributes, fields) of the type. Within patterns
-- one can use the product notation or explicitly match against named fields of the record.
-- For example, the following fragment:
-- @
-- f Note { pitch = p } = ...
-- @
-- binds the @pitch@ value of a parameter of type Node to p, ignoring other fields.
data Note = Note {
pitch :: Pitch,
octave :: Octave,
duration :: Duration
} deriving (Eq,Ord,Show,Read)

-- | A chord could be alternatively defined as another form of Note, using a different
-- constructor.
data Chord = Chord [Note] Duration
deriving (Eq,Ord,Show,Read)

-- | A type-class declaration.
-- Type-classes can be thought of as both:
--
-- 1. An interface (in the Java sense) defining some related functions over a specific
-- type,
--
-- 2. A constraint over the possible types during type inference: Using a function
-- defined in a type-class effectively restricts the possible types occuring as
-- arguments or return values of this function to some member of type-class which
-- has consequences for callers.
--
-- Here we use the "simple" Haskell 98 syntax with a single variable.
class Playable p where
interpret :: Tempo -> p -> Wave

Expand All @@ -43,6 +83,8 @@ largo = 40 :: Int
-- see http://en.wikipedia.org/wiki/Note for formula
frequency p = truncate $ 2 ** (fromIntegral (fromEnum p - fromEnum A) / 12) * 440

-- |Define value function as an enumerated case over possible instances of Duration.
-- Note the *tabular* structure of this definition.
value Semiquaver = 1/4
value Quaver = 1/2
value Crotchet = 1
Expand All @@ -54,6 +96,7 @@ value (Pointed d) = value d * 1.5
chord :: Note -> Note -> Chord
chord n n' = Chord [n,n'] (max (duration n) (duration n'))

-- |A possible instance of Playable for Chord type.
instance Playable Chord where
interpret tempo (Chord ns d) = slice (durationInSeconds tempo d) $ foldl1 (°) (map (interpret tempo) ns)

Expand Down
92 changes: 89 additions & 3 deletions Sound.hs
Original file line number Diff line number Diff line change
@@ -1,33 +1,119 @@
-- One of many possible language extensions
-- Here is the error we get from the compiler when we remove this clause:
--
-- Sound.hs:74:9:
-- Ambiguous type variable `a0' in the constraints:
-- (RealFrac a0) arising from a use of `truncate' at Sound.hs:74:9-16
-- (Num a0) arising from a use of `*' at Sound.hs:74:28
-- (Integral a0) arising from a use of `fromIntegral'
-- at Sound.hs:36:25-36
-- (Enum a0) arising from the arithmetic sequence `0 .. n'
-- at Sound.hs:36:47-54
-- Possible cause: the monomorphism restriction applied to the following:
-- computeSound :: forall a.
-- (Ord a, Floating a) =>
-- a0 -> a0 -> a -> [a]
-- (bound at Sound.hs:86:1)
-- slice :: forall a. a0 -> [a] -> [a] (bound at Sound.hs:73:1)
-- wave :: forall b. Floating b => a0 -> [b] (bound at Sound.hs:21:1)
-- samplingRate :: a0 (bound at Sound.hs:17:1)
-- Probable fix: give these definition(s) an explicit type signature
-- or use -XNoMonomorphismRestriction
-- In the expression: truncate
-- In the first argument of `take', namely
-- `(truncate $ seconds * samplingRate)'
-- In the expression:
-- take (truncate $ seconds * samplingRate) repeatWave
--
-- Monomorphism restriction's definition is rather complex (see Haskell Report 4.5.5 for details)
-- but its meaning is quite simple: When a type variable is free within a type expression it
-- cannot be generalized (eg. implicitly universally quantified) even if this would be perfectly legal. Here we do not
-- have an explicit type for @samplingRate@ hence it's type is a variable which occurs free in the reported
-- context, hence it cannot be generalized and becomes "ambiguous" when used in two places with two different
-- possible types. Another way to fix the problem would be to declare explicitly a type for sampling rate:
--
-- samplingRate :: (Num a) => a
--

{-# LANGUAGE NoMonomorphismRestriction #-}

{-| A module for low-level sound generation routines.
By default modules export all the symbols they define.
-}
module Sound where

-- | A simple type alias.
-- Type aliases are stricly syntactic: The symbol is replaced by its definition before
-- typechecking. Common aliases are
-- @@
-- type String = [Char]
-- @@
type Wave = [Double]

-- this a CAF: Constant Applicative Form
-- | This a CAF: Constant Applicative Form.
samplingRate = 44000

-- a sinusoidal wave between -1 and +1
-- | A function producing a sinusoidal sampling of a given frequency
-- depending on the sampling rate.
wave frequency =
-- @let@ keyword introduces variable definition scope. For all practical purpose
-- a let scope can contain any definition that would be valid at the toplevel but
-- of course the symbols are visible only within the scope of the let environment.
-- Note that mutually recursive definitions are perfectly legal in a let environment,
-- just like it is legal at the top-level.
--
-- Note here the use of a symbol in infix notation using backquotes. All binary function
-- symbols may be used as operators using backquotes. Conversely, all operators may be
-- used in prefix form using parens.
let n = samplingRate `div` frequency
in map (sin . (* (2 * pi))) [ fromIntegral i / fromIntegral n | i <- [0 .. n]]
in map
(sin . (* (2 * pi))) -- a common idiom in Haskell: Using "function pipelines" in so-called
-- point-free notation. The use of a partially applied operators @(* x)@
-- is called a _section_.
[ fromIntegral i / fromIntegral n | i <- [0 .. n]] -- a list comprehension, similar to a for-loop in Scala


-- |Defining an operator is identical to declaring a function.
-- An operator is any (valid) symbol which does not start with an alphabetic letter.
-- Note the use of an explicit type declaration which is never mandatory excepts in
-- situation where there is an ambiguity (for example due to conflicting type-classes existing
-- in scope)
(°) :: Wave -> Wave -> Wave
-- Operator's definition can use infix notation.
w ° w' = zipWith avg w1 w2
-- a @where@ clause is similar to a let-environment excepts its scope is the preceding expression.
where
avg a b = (a + b) /2
w1 = w ++ w1
w2 = w' ++ w2

-- |A typical definition of a recursive function using pattern-matching.
-- There is a very close match with inductive proofs in mathematics and practically this
-- form is quite useful to prove properties or derive more efficient forms from an existing
-- definition. However the order of clauses is important when they may overlap as this gets translated into
-- a sequence of possibly nested cases. The compiler can however issue warnings when it
-- it detects overlapping clauses (which could be the case here as 0 is a special case of n).
duplicate :: Int -> [a] -> [a]
-- define the behaviour for base case which usually stops the recursion
duplicate 0 l = l
-- recursively call the function, "reducing" step-by-step its scope until
-- it reaches the base case.
duplicate n l = l ++ duplicate (n-1) l

-- | This function's definition uses a *guard* to select cases depending on
-- the value of an expression, not only the structure of the parameters. All
-- the variables defined in patterns are in scope in the guard.
amplitude ratio | ratio > 0 && ratio < 1 = map (*ratio)
-- otherwise is simply an alias for True.
| otherwise = id

slice seconds wave =
take (truncate $ seconds * samplingRate) repeatWave
where
-- We take advantage of laziness to define concisely and efficiently a repeating structure
-- This expression and each subexpression will be evaluated only if its value is actually
-- needed at runtime to make progress in the computation (of the top-level main function).
repeatWave = wave ++ repeatWave

-- |Scale a list of doubles between -1 and 1 to an integer interval
Expand Down
41 changes: 39 additions & 2 deletions SoundIO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@
module SoundIO where
import Music
import Sound
-- imports can be qualified to use a shortcut prefix. This allows disambiguating
-- functions occuring in several modules.
-- Note that by default we import every symbol from a module when it is used
-- unqualified. Qualifying it implies that no symbol is imported by default.
import qualified Data.ByteString.Char8 as B
import System.Process(createProcess, shell, CreateProcess(..), StdStream(..))
-- We can explicitly restrict the set of symbols imported from a module
import System.Process(createProcess, shell,
-- import a type and all its constructors.
CreateProcess(..),
StdStream(..))
import System.IO(hSetBuffering, hSetBinaryMode, hPutStrLn, hGetLine, stdin, hFlush, stdout, BufferMode(..))
-- Package-qualified imports resolves ambiguity when two modules export the same symbol.
-- This is generally a bad idea and a sign that the build environment is somehow broken
-- or in bad shape.
import "monads-tf" Control.Monad.State(State(..),get,put,runState)
import qualified Data.Map as Map

Expand All @@ -21,11 +32,34 @@ prepareSound = B.pack.map toEnum.scale (0,255)
outputSound = B.putStr. prepareSound
note (p,o,d) = Note p o d


-- | A command that uses the "dreaded" State monad to maintain a "state".
-- A function operates "in" a monad M when its return type is M X for some X.
-- The State type definition is something similar to the following:
-- @
-- newtype State s a = State { runState :: s -> (a, s) }
-- @
-- In other words, it encapsulates a function that takes a state value @s@ and
-- returns a value @a@ together with a possibly updated state @s@. Sequencing
-- operations within the State monad hides the details of passing the state @s@
-- from one function to another hence propagating "mutations" down the chain.
command :: String -> State Store CommandResult

-- here we use a View pattern (a language extension) as a pattern in the clause:
-- The function @words@ is called with the actual argument to command as as its
-- argument, and its result is matched against the pattern on
-- the right of the @->@ symbol.
command (words -> ["load",name,file]) = do
-- @get@ is a monadic operation in the State monad that exposes the current value of
-- the state @s@. The arrow @<-@ is part of the special monad notation (just like the
-- @do@ keyword.
store <- get
let store' = Map.insert name file store
-- @put@ modifies the current state using its argument
put store'
-- @return@ is the standard Monad function for producing a result in the Monad. Its
-- name is purposefully confusing with the @return@ of imperative languages but the
-- semantic is very different.
return Loaded
command (words -> ["play",name]) = do
store <- get
Expand All @@ -39,6 +73,7 @@ command c = return $ Error $ "'" ++ c ++ "' is not a valid command"
prompt = do putStr "> "
hFlush stdout

-- | Command-loop is another monadic computation, this time in the IO monad.
commandLoop :: Store -> IO ()
commandLoop s = do
cmd <- hGetLine stdin
Expand All @@ -52,7 +87,9 @@ commandLoop s = do
scoreData <- readFile file
playSound $ (map note.read) scoreData

-- use external program 'aplay' to generate sound
-- | Use external program 'aplay' to generate sound.
-- Haskell has a rich set of functions to interact with external processes and
-- system, which are rather simple to use.
playSound :: (Playable a) => [a] -> IO ()
playSound sounds = do
let procDef = (shell $ "aplay -r " ++ (show samplingRate)) { std_in = CreatePipe }
Expand Down
37 changes: 22 additions & 15 deletions synthesizer.hs
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
{- Command-line interface.
By convention program entry point is the symbol @main@ defined in the
module @Main@. Implicitly an unnamed module is Main.
-}
import System.Environment (getArgs)
import Sound
import Music
import SoundIO

{-|
We define various Constant Applicative Forms that can be used to
to generate music. Variables in Haskell are immutable and referentially
transparent: One can always replace the variable with the expression
it denotes. This implies that CAF and local variables value is memoized
upon its first evaluation.
-}
simplenote p = Note p 4 Crotchet

harrypotter = [(B,4,Crotchet),
(E,5,Pointed Crotchet),
(G,5,Quaver),
(Fs,5,Crotchet),
(E,5,Minim),
(B,5,Crotchet),
(A,5,Minim),
(Fs,5,Minim),
(E,5,Pointed Crotchet),
(G,5,Quaver),
(Fs,5,Crotchet),
(Ds,5,Minim),
(F,5,Crotchet),
(B,4,Semibreve)]

cMajor = Chord (map note [(C,4,Crotchet),
(E,4,Crotchet),
(G,4,Crotchet)]) Crotchet
Expand All @@ -31,10 +28,20 @@ cMinor = Chord (map note [(C,4,Crotchet),
-- main = do
-- outputSound $ (interpret 60 cMajor) ++ (interpret 60 cMinor)

{-|
Main symbol has type @IO ()@: It is a computation in the IO monad with
no interesting result.
-}
main = do
prompt
commandLoop emptyStore

{-|
This version of @main@ demonstrates extraction of command-line arguments: We simply
invoke @getArgs@ which outputs a list of strings and we pattern match against the
expected number of arguments. In real-life situation, we would want to use a dedicated
utility such as @System.Console.GetOpt@ for handling arguments.
-}
-- main = do
-- [frequency,volume,duration] <- getArgs
-- let f = read frequency :: Int
Expand Down
17 changes: 16 additions & 1 deletion websynth.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
-- | A module for sample web server providing wave generation from
-- a score in Readable form.

-- We use the OverloadedStrings extension in order to benefit from more
-- efficient string implementation provided by Data.ByteString.Char8 module.
-- By default in Haskell, a String is a list of Char which is extremely
-- inefficient as a Char is a full Unicode 32-bit number.

{-# LANGUAGE OverloadedStrings, PackageImports #-}

-- miku is one of many recent web frameworks that try to offer the same
-- ease of use than what's available in more dynamic languages like python
-- or ruby.
import Network.Miku(miku,html,get,post)
import Network.Miku.Utils(update)
import Hack2.Contrib.Request(input_bytestring)
Expand All @@ -9,7 +20,8 @@ import Hack2.Contrib.Request(params)
import "monads-tf" Control.Monad.Reader(ask)
import "monads-tf" Control.Monad.Trans(liftIO)

-- Templating stuff
-- Templating stuff. Blaze provides a rich set of combinators (functions) to
-- build objects representing HTML structure in a typed and efficient way.
import Blaze.ByteString.Builder
import Text.Blaze.Html5 hiding (map, html)
import Text.Blaze.Html5.Attributes hiding (form,label)
Expand All @@ -33,6 +45,9 @@ mainPage = docTypeHtml $
input ! type_ "submit" ! value "Submit"

main = run . miku $ do
-- miku provides a dedicated monad for expressing routing rules based on standard
-- http queries structure. Each operation in the monad is a rule that gets matched
-- by incoming request in order and returns a value.
get "/" (html $ toByteString $ renderHtmlBuilder mainPage)
get "/synthesize" $ do
env <- ask
Expand Down

0 comments on commit b4a1521

Please sign in to comment.