Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

added comments for haskell newbies

  • Loading branch information...
commit b4a15211d87e26910bc4c8b6e1b610b71c10b056 1 parent 2376b82
Arnaud Bailly authored
Showing with 211 additions and 23 deletions.
  1. +45 −2 Music.hs
  2. +89 −3 Sound.hs
  3. +39 −2 SoundIO.hs
  4. +22 −15 synthesizer.hs
  5. +16 −1 websynth.hs
View
47 Music.hs
@@ -4,7 +4,14 @@ 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 |
@@ -12,9 +19,18 @@ data Pitch = C | Cs |
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 |
@@ -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
@@ -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
@@ -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)
View
92 Sound.hs
@@ -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
View
41 SoundIO.hs
@@ -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
@@ -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
@@ -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
@@ -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 }
View
37 synthesizer.hs
@@ -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
@@ -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
View
17 websynth.hs
@@ -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)
@@ -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)
@@ -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
Please sign in to comment.
Something went wrong with that request. Please try again.