Contribution:
haskellLogging
Headline
Logging in Haskell with non-monadic code
Characteristics
Starting from a straightforward family of functions for cutting salaries, the concern of logging the salary changes is incorporated into the functions such that the function results are enriched by the log entries for salary changes. This code is relatively verbose and implies poor abstraction. In particular, functionality for composing logs is scattered all over the functions. Ultimately, such a problem must be addressed with monads.
Illustration
Salary changes can be tracked in logs as follows:
type Log = [LogEntry]
data LogEntry =
LogEntry {
name :: String,
oldSalary :: Float,
newSalary :: Float
}
deriving (Show)
Here are a few entries resulting from a salary cut for the sample company:
[LogEntry {name = "Craig", oldSalary = 123456.0, newSalary = 61728.0},
LogEntry {name = "Erik", oldSalary = 12345.0, newSalary = 6172.5},
LogEntry {name = "Ralf", oldSalary = 1234.0, newSalary = 617.0},
LogEntry {name = "Ray", oldSalary = 234567.0, newSalary = 117283.5},
LogEntry {name = "Klaus", oldSalary = 23456.0, newSalary = 11728.0},
LogEntry {name = "Karl", oldSalary = 2345.0, newSalary = 1172.5},
LogEntry {name = "Joe", oldSalary = 2344.0, newSalary = 1172.0}]
Given a log, the median of salary deltas can be computed as follows:
log2median :: Log -> Float
log2median = median . log2deltas
log2deltas :: Log -> [Float]
log2deltas = sort . map delta
where
delta entry = newSalary entry - oldSalary entry
The above log reduces to the following median:
-6172.5
Feature:Cut is implemented in logging-enabled fashion as follows:
cut :: Company -> (Company, Log)
cut (Company n ds) = (Company n ds', log)
where
(ds', logs) = unzip (map cutD ds)
log = concat logs
cutD :: Department -> (Department, Log)
cutD (Department n m ds es)
= (Department n m' ds' es', log)
where
(m',log1) = cutE m
(ds', logs2) = unzip (map cutD ds)
(es', logs3) = unzip (map cutE es)
log = concat ([log1]++logs2++logs3)
cutE :: Employee -> (Employee, Log)
cutE (Employee n a s) = (e', log)
where
e' = Employee n a s'
s' = s/2
log = [ LogEntry {
name = n,
oldSalary = s,
newSalary = s'
} ]
Thus, all functions return a regular data item (i.e., some part of the company) and a corresponding log. When lists of company parts are processed with map, then the lists of results must be unzipped (to go from a list of pairs to a pair of lists). In the function for departments, multiple logs arise for parts a department; these intermediate logs must be composed.
Relationships
- See Contribution:haskellComposition for the corresponding contribution that does not yet involve logging. The data model is preserved in the present contribution, but the functions for cutting salaries had to be rewritten since the logging concern crosscuts the function.
- See Contribution:haskellWriter for a variation on the present contribution, which leverages a writer monad, though, for conciseness and proper abstraction.
Architecture
There are these Haskell modules:
- Company.hs: the data model reused from Contribution:haskellComposition.
- Cut.hs: the combined implementation of Feature:Cut and Feature:Logging.
- Log.hs: types and functions for logs of salary changes needed for Feature:Logging.
- Main.hs: demonstration of all functions.
Contribution:
haskellComposition
Headline
Data composition in Haskell with algebraic data types
Characteristics
The data model leverages data composition for companies with departmental nesting. Thus, an algebraic data type is used for departments so that recursive nesting can be expressed. The algebraic data type only needs a single data constructor. Thus, data variation is not exercised, but see Contribution:haskellVariation for an alternative with data variation.
Illustration
The data model leverages an algebraic data type for departments; in this manner recursion is enabled:
{-| A data model for the 101companies System -}
module Company.Data where
-- | A company consists of name and top-level departments
type Company = (Name, [Department])
-- | A department consists of name, manager, sub-departments, and employees
data Department = Department Name Manager [Department] [Employee]
deriving (Eq, Read, Show)
-- | An employee consists of name, address, and salary
type Employee = (Name, Address, Salary)
-- | Managers as employees
type Manager = Employee
-- | Names of companies, departments, and employees
type Name = String
-- | Addresses as strings
type Address = String
-- | Salaries as floats
type Salary = Float
A sample company looks like this:
{- | Sample data of the 101companies System -}
module Company.Sample where
import Company.Data
-- | A sample company useful for basic tests
sampleCompany :: Company
sampleCompany =
( "Acme Corporation",
[
Department "Research"
("Craig", "Redmond", 123456)
[]
[
("Erik", "Utrecht", 12345),
("Ralf", "Koblenz", 1234)
],
Department "Development"
("Ray", "Redmond", 234567)
[
Department "Dev1"
("Klaus", "Boston", 23456)
[
Department "Dev1.1"
("Karl", "Riga", 2345)
[]
[("Joe", "Wifi City", 2344)]
]
[]
]
[]
]
)
Feature:Total is implemented as follows:
{-| The operation of totaling all salaries of all employees in a company -}
module Company.Total where
import Company.Data
-- | Total all salaries in a company
total :: Company -> Float
total (_, ds) = totalDepartments ds
where
-- Total salaries in a list of departments
totalDepartments :: [Department] -> Float
totalDepartments [] = 0
totalDepartments (Department _ m ds es : ds')
= getSalary m
+ totalDepartments ds
+ totalEmployees es
+ totalDepartments ds'
where
-- Total salaries in a list of employees
totalEmployees :: [Employee] -> Float
totalEmployees [] = 0
totalEmployees (e:es)
= getSalary e
+ totalEmployees es
-- Extract the salary from an employee
getSalary :: Employee -> Salary
getSalary (_, _, s) = s
The following salary total is computed for the sample company:
399747.0
Relationships
- See Contribution:haskellVariation for a contribution with a similar data model such that data variation is exercised in addition to data composition.
- See Contribution:haskellEngineer for a contribution with a simple data model without support for departmental nesting. No algebraic data types are leveraged.
- See Contribution:haskellData for a contribution with a simple data model without support for departmental nesting. Algebraic data types are leveraged systematically for all types to distinguish the types nominally.
Architecture
There are these modules:
{-| A data model for the 101companies System -}
module Company.Data where
-- | A company consists of name and top-level departments
type Company = (Name, [Department])
-- | A department consists of name, manager, sub-departments, and employees
data Department = Department Name Manager [Department] [Employee]
deriving (Eq, Read, Show)
-- | An employee consists of name, address, and salary
type Employee = (Name, Address, Salary)
-- | Managers as employees
type Manager = Employee
-- | Names of companies, departments, and employees
type Name = String
-- | Addresses as strings
type Address = String
-- | Salaries as floats
type Salary = Float
{- | Sample data of the 101companies System -}
module Company.Sample where
import Company.Data
-- | A sample company useful for basic tests
sampleCompany :: Company
sampleCompany =
( "Acme Corporation",
[
Department "Research"
("Craig", "Redmond", 123456)
[]
[
("Erik", "Utrecht", 12345),
("Ralf", "Koblenz", 1234)
],
Department "Development"
("Ray", "Redmond", 234567)
[
Department "Dev1"
("Klaus", "Boston", 23456)
[
Department "Dev1.1"
("Karl", "Riga", 2345)
[]
[("Joe", "Wifi City", 2344)]
]
[]
]
[]
]
)
{-| The operation of totaling all salaries of all employees in a company -}
module Company.Total where
import Company.Data
-- | Total all salaries in a company
total :: Company -> Float
total (_, ds) = totalDepartments ds
where
-- Total salaries in a list of departments
totalDepartments :: [Department] -> Float
totalDepartments [] = 0
totalDepartments (Department _ m ds es : ds')
= getSalary m
+ totalDepartments ds
+ totalEmployees es
+ totalDepartments ds'
where
-- Total salaries in a list of employees
totalEmployees :: [Employee] -> Float
totalEmployees [] = 0
totalEmployees (e:es)
= getSalary e
+ totalEmployees es
-- Extract the salary from an employee
getSalary :: Employee -> Salary
getSalary (_, _, s) = s
{-| The operation of cutting all salaries of all employees in a company in half -}
module Company.Cut where
import Company.Data
-- | Cut all salaries in a company
cut :: Company -> Company
cut (n, ds) = (n, (map cutD ds))
where
-- Cut all salaries in a department
cutD :: Department -> Department
cutD (Department n m ds es)
= Department n (cutE m) (map cutD ds) (map cutE es)
where
-- Cut the salary of an employee in half
cutE :: Employee -> Employee
cutE (n, a, s) = (n, a, s/2)
{-| Tests for the 101companies System -}
module Main where
import Company.Data
import Company.Sample
import Company.Total
import Company.Cut
import Test.HUnit
import System.Exit
-- | Compare salary total of sample company with baseline
totalTest = 399747.0 ~=? total sampleCompany
-- | Compare total after cut of sample company with baseline
cutTest = total sampleCompany / 2 ~=? total (cut sampleCompany)
-- | Test for round-tripping of de-/serialization of sample company
serializationTest = sampleCompany ~=? read (show sampleCompany)
-- | The list of tests
tests =
TestList [
TestLabel "total" totalTest,
TestLabel "cut" cutTest,
TestLabel "serialization" serializationTest
]
-- | Run all tests and communicate through exit code
main = do
counts <- runTestTT tests
if (errors counts > 0 || failures counts > 0)
then exitFailure
else exitSuccess
{-| A data model for the 101companies System -}
module Company.Data where
-- | A company consists of name and top-level departments
type Company = (Name, [Department])
-- | A department consists of name, manager, sub-departments, and employees
data Department = Department Name Manager [Department] [Employee]
deriving (Eq, Read, Show)
-- | An employee consists of name, address, and salary
type Employee = (Name, Address, Salary)
-- | Managers as employees
type Manager = Employee
-- | Names of companies, departments, and employees
type Name = String
-- | Addresses as strings
type Address = String
-- | Salaries as floats
type Salary = Float
Usage
See https://github.com/101companies/101haskell/blob/master/README.md.
Concept:
Monad
Headline
A functional programming idiom for computing effects
Illustration
The term "monad" originates from category theory, but this illustration focuses on the functional programming view where "monad" refers to a programming idiom for composing computations, specifically computations that may involve side effects or I/O actions. Monads have been popularized by Language:Haskell.
In Haskell, monads are developed and used with the help of the type class Monad which is parametrized by a type constructor for the actual monad. Here is a sketch of the type class:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
-- ... some details omitted
The return function serves the construction of trivial computations, i.e., computations that return values. The >>= (also knows as the bind function) compose a computation with a function that consumes the value of said computation to produce a composed computation. Here are some informal descriptions of popular monads:
- State monad
- return v: return value v and pass on state
- bind c f: apply computation c as state transformer and pass on transformed state to f
- Reader monad
- return v: return value v and ignore environment
- bind c f: pass environment to both c and f
- Writer monad
- return v: return value v and empty output
- bind c f: compose output from both c and f
- Maybe monad
- return v: return "successful" value v
- bind c f: fail if c fails, otherwise, pass on successful result to f
Contribution:
haskellWriter
Headline
Logging in Haskell with the Writer monad
Characteristics
Salary changes are logged in a Language:Haskell-based implementation with the help of a writer monad. Compared to a non-monadic implementation, the code is more concise. Details of logging are localized such that they only surface in the context of code that actually changes salaries.
Illustration
See Contribution:haskellLogging for a simpler, non-monadic implementation.
The present, monadic implementation differs only with regard to the cut function:
cut :: Company -> Writer Log Company
cut (Company n ds) =
do
ds' <- mapM cutD ds
return (Company n ds')
where
cutD :: Department -> Writer Log Department
cutD (Department n m ds es) =
do
m' <- cutE m
ds' <- mapM cutD ds
es' <- mapM cutE es
return (Department n m' ds' es')
where
cutE :: Employee -> Writer Log Employee
cutE (Employee n a s) =
do
let s' = s/2
let log = [ LogEntry {
name = n,
oldSalary = s,
newSalary = s'
} ]
tell log
return (Employee n a s')
Thus, the family of functions uses a writer monad in the result types. The sub-traversals are all composed by monadic bind (possibly expressed in do-notation). The function for processing departments totally abstracts from the fact that logging is involved. In fact, that function could be defined to be parametrically polymorphic in the monad at hand.
Relationships
- See Contribution:haskellComposition for the corresponding contribution that does not yet involve logging. The data model is preserved in the present contribution, but the functions for cutting salaries had to be rewritten since the logging concern crosscuts the function.
- See Contribution:haskellLogging for a variation on the present contribution which does not yet use monadic style.
Architecture
See Contribution:haskellLogging.
Feature:
Logging
Headline
Log and analyze salary changes
Description
Salaries of employees may change over time. For instance, a salary cut systematically decreases salaries. Of course, a pay raise could also happen; point-wise salary changes are conceivable as well. Salary changes are to be logged so that they can be analyzed within some window of interest. Specifically, a salary cut is to be logged with names of affected employees, salary before the change, and salary after the change. The log is to be analyzed in a statistical manner to determine the median and the mean of all salary deltas.
Motivation
The feature requires logging of updates to employee salaries. Depending on the programming language at hand, such logging may necessitate revision of the code that changes salaries. Specifically, logging of salary changes according to a salary cut may necessitate adaptation of the actual transformation for cutting salaries. Logging should be preferably added to a system while obeying separation of concerns. So logging is potentially a crosscutting concern, which may end being implemented in a scattered manner, unless some strong means of modularization can be adopted.
Illustration
The log for salary cut for the "standard" sample company would look as follows.
[ LogEntry {name = "Craig", oldSalary = 123456.0, newSalary = 61728.0}, LogEntry {name = "Erik", oldSalary = 12345.0, newSalary = 6172.5}, LogEntry {name = "Ralf", oldSalary = 1234.0, newSalary = 617.0}, LogEntry {name = "Ray", oldSalary = 234567.0, newSalary = 117283.5}, LogEntry {name = "Klaus", oldSalary = 23456.0, newSalary = 11728.0}, LogEntry {name = "Karl", oldSalary = 2345.0, newSalary = 1172.5}, LogEntry {name = "Joe", oldSalary = 2344.0, newSalary = 1172.0} ]
For what it matters, the salary cut operates as a depth-first, left-to-right traversal of the company; thus the order of the entries in the log. Projection of changes to deltas and sorting them results in the following list of deltas:
[ -117283.5, -61728.0, -11728.0, -6172.5, -1172.5, -1172.0, -617.0 ]
Clearly, the median is the element in the middle:
-6172.5
By contrast, the mean is much different because of the skewed distribution of salaries:
-28553.355
See Contribution:haskellLogging for a simple implementation of the feature in Language:Haskell.
Relationships
- The present feature builds on top of Feature:Cut, as it is required to demonstrate the analysis of logged deltas for the transformation of a salary cut.
- The present feature should be applicable to any data model of companies, specifically Feature:Flat company and Feature:Hierarchical company.
Guidelines
- The name of the type for logs should involve the term "log".
- A suitable demonstration of the feature's implementation should cut the sample company and compute the median of the salary deltas, as indeed stipulated above.
Feature:
Cut
Headline
Cut the salaries of all employees in half
Description
For a given company, the salaries of all employees are to be cut in half. Let's assume that the management of the company is interested in a salary cut as a response to a financial crisis. Clearly, any real company is likely to respond to a financial crisis in a much less simplistic manner.
Motivation
The feature may be implemented as a transformation, potentially making use of a suitable transformation or data manipulation language. Conceptually, the feature corresponds to a relatively simple and regular kind of transformation, i.e., an iterator-based transformation, which iterates over a company' employees and updates the salaries of the individual employees along the way. It shall be interesting to see how different software languages, technologies, and implementations deal with the conceptual simplicity of the problem at hand.
Illustration
The feature is illustrated with a statement in Language:SQL to be applied to an instance of a straightforward relational schema for companies where we assume that all employees belong to a single company:
UPDATE employee
SET salary = salary / 2;
The snippet originates from Contribution:mySqlMany.
Relationships
- See Feature:Total for a query scenario instead of a transformation scenario.
- In fact, Feature:Total is likely to be helpful in a demonstration of Feature:Salary cut.
- The present feature should be applicable to any data model of companies, specifically Feature:Flat company and Feature:Hierarchical_company.
Guidelines
- The name of an operation for cutting salaries thereof should involve the term "cut". This guideline is met by the above illustration, if we assume that the shown SQL statement is stored in a SQL script with name "Cut.sql". Likewise, if OO programming was used for implementation, then the names of the corresponding methods should involve the term "cut".
- A suitable demonstration of the feature's implementation should cut the salaries of a sample company. This guideline is met by the above illustration, if we assume that the shown SQL statement is executed on a database which readily contains company data. Queries according to Feature:Total may be used to compare salaries before and after the cut. All such database preparation, data manipulation, and query execution should preferably be scripted. By contrast, if OO programming was used, then the demonstration could be delivered in the form of unit tests.
Concept:
Logging
Headline
The capability of logging events along program execution
Discussion
Logging may be meant to help with debugging, forensics, or data governance.
Logging may be achieved by designated, possibly scattered code units.
A popular approach to logging is provided by Aspect-oriented programming.
Language:
Haskell
Headline
The functional programming language Haskell
Details
101wiki hosts plenty of Haskell-based contributions. This is evident from corresponding back-links. More selective sets of Haskell-based contributions are organized in themes: Theme:Haskell data, Theme:Haskell potpourri, and Theme:Haskell genericity. Haskell is also the language of choice for a course supported by 101wiki: Course:Lambdas_in_Koblenz.
Illustration
The following expression takes the first 42 elements of the infinite list of natural numbers:
> take 42 [0..]
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41]
In this example, we leverage Haskell's lazy evaluation.