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:

The contribution relies on the hackage package hstats.


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

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
: a data model for Feature:Hierarchical company
{- | 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)]
            ]
            []
        ]
        []
    ]
  )
: a sample company
{-| 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 implementation of Feature:Total
{-| 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)
: the implementation of Feature:Cut
{-| 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
: Tests The types of
{-| 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
implement Feature:Closed serialization through Haskell's read/show.

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

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

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.