Functional Programming is the programming paradigm where you program without using variables. Functions call other functions with the variable passing between.
This paradigm allows for constants and strong type casting. It is also characterized with lazy evaluation: expressions are not evaluated until a result is required.
Haskell is a functional programming language that has been around since 1987. It has a great community of programmers behind it and is available on the Mac, Windows, and Linux platforms.
In this tutorial, I’ll create an Alfred library to help make workflows in Haskell. Then I am going to create a workflow using this library.
In this tutorial, I’ll assume that you’re already familiar with writing workflows in Alfred. If not, please check out these tutorials:
Since a complete description of Haskell programming is outside the scope of this tutorial, you can read A Gentile Introduction to Haskell: Version 98. I personally learned Haskell from the free book Write Yourself a Scheme in 48 Hours. I like to learn new languages by writing a program that is useful.
In the Preferences Panel, click on the Locations item. At the bottom, make sure the Command Line Tools is pointing to your version of XCode. It will ask for a user login. Once given, the command line tools are set up to use.
With XCode installed and set up, download the Haskell compiler and support files from Haskell for Mac OS X.
After unzipping the file, move the ghc-7.10.1.app to the Applications folder (7.10.1 was the latest version as I wrote this tutorial).
The installer shows the code to add to your ~/.bash_profile file and your ~/.zshrc file (if you use Z-shell). Once you add that code to your shell files, click the Refresh Checklist. All of the checkboxes should be green. You are now ready to create Haskell programs.
Alfred Library in Haskell
Haskell has a package manager similar to NodeJS’s npm. It is cabal. With cabal you can download and install many third-party libraries that others have written in Haskel. For the Alfred Haskell Library, you need to install the TagSoup library. This helps retrieve the bundleid of the Alfred workflow. On the command line, type:
cabal install tagsoup
Now create a new file called
Alfred.hs. All Haskell program files end with the
.hs extension. In this file, start placing this code:
module Alfred ( begin , end , addItem , addItemBasic , getAlfredDataFileContents , putAlfredDataFileContents , getAlfredCacheFileContents , putAlfredCacheFileContents ) where import System.Process import System.Directory import System.Environment import Text.HTML.TagSoup
module Alfred ( starts a module. All Haskell files are modules. Inside the parenthesis defines the functions that this module will make available. All Haskell functions start with a lower case letter. If the module is to be the main program, it has to have a
main function. Since this is a library, it just has functions and constants declarations.
After declaring the
module and what it exports, you write the list of the different libraries used. This library makes use of three libraries:
System.Environment, and the
Text.HTML.TagSoup that we downloaded with
cacheDirBasic :: String cacheDirBasic = "/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/" dataDirBasic :: String dataDirBasic = "/Library/Application Support/Alfred 2/Workflow Data/" begin :: String begin = "<?xml version=\"1.0\"?><items>" end :: String end = "</items>"
Next, I define three constants. The first line is the type declaration with the actual definition below it. The
:: symbol declares that the next items are type declarations. Here, we are defining the constants as a
addItem :: String -> String -> String -> String -> String -> String -> String -> String addItem a b c d e f g = "<item uid=\"" ++ a ++ "\" arg=\""++ b ++ "\" valid=\""++ c ++ "\" autocomplete=\"" ++ d ++ "\"><title>" ++ e ++ "</title><subtitle>" ++ f ++ "</subtitle><icon>" ++ g ++ "</icon></item>"
addItem function takes seven
String inputs and creates the XML item
String as output. Notice the type declaration looks like eight
String types separated by
-> symbol shows the next type. The first seven types are the input with the last one being the return value. All functions in Haskell have to have a return value.
addItemBasic :: String -> String -> String -> String -> String addItemBasic a b c d = addItem a b "yes" "" c d "icon.png"
addItemBasic uses the
addItem function to create an XML item with three of the inputs set to a default value.
getBundleID :: IO (String) getBundleID = do tags <- fmap parseTags $ readFile "info.plist" let id = fromTagText $ dropWhile (~/= "<string>") (partitions (~== "<dict>") tags !! 0) !! 1 return id
getBundleID get the ID of the Alfred Workflow bundle from the info.plist file in the workflow directory. This file is a plain text
plist. Since this is a type of XML or HTML format, TextSoup library works real well to read it in and decode it. This is a monad function (simply put, a function that interacts with the item outside the program. For instance: files, GUI, etc) You can learn more about monads in the monads tutorial. Any function with the return type of
IO () is a monad for input or output functions.
Here, the functional programming style breaks down to a list of items. The
do keyword says, “do the following in order”.
The first line after the
do reads in the info.plist file and parsed out the tags using
parseTags from the TextSoup library. The second line finds the first
dict tag and drops everything until the
string tag. It then extracts the tags contents and places it in the trasistional constant
id is a constant because it’s value can not change. Since all Alfred info.plist files have the exact same layout, I do not need to test the key to be correct. The
id value is then returned.
getAlfredDataFileContents :: String -> IO (String) getAlfredDataFileContents fileName = do h <- getHomeDirectory id <- getBundleID let fPath = h ++ dataDirBasic ++ id ++ "/" ++ fileName fExist <- doesFileExist fPath if fExist then do contents <- readFile fPath return contents else return ""
The next monad function gets the contents of a file that is in the Alfred Data Directory for the workflow and returns it to the calling function. The function first gets the user’s home dirctory, gets the bundle ID, creates a path to the Alfred Data Directory for the workflow, and returns the contents of that file if it exists. Otherwise, the function returns an empty string.
putAlfredDataFileContents :: String -> String -> IO () putAlfredDataFileContents fileName dataStr = do h <- getHomeDirectory id <- getBundleID writeFile (h ++ dataDirBasic ++ id ++ "/" ++ fileName) dataStr
The next monad does the exact opposite. It takes a file name and the data to store in that file. Since the contents will be put in to the file named whether or not it exists, the existence of the file does not need to be determined.
getAlfredCacheFileContents :: String -> IO (String) getAlfredCacheFileContents fileName = do h <- getHomeDirectory id <- getBundleID let fPath = h ++ cacheDirBasic ++ id ++ "/" ++ fileName fExist <- doesFileExist fPath if fExist then do contents <- readFile fPath return contents else return "" putAlfredCacheFileContents :: String -> String -> IO () putAlfredCacheFileContents fileName dataStr = do h <- getHomeDirectory id <- getBundleID writeFile (h ++ cacheDirBasic ++ id ++ "/" ++ fileName) dataStr
The last two monad function in the library reads and writes files in the Alfred Cache directory for the workflow.
Haskell Case Converter
With the library created, it is to write a workflow. Create a new workflow in Alfred called Haskell Text Converter as shown:
The Script Filter should look like this:
Now, click on Open workflow folder. This opens the folder for the workflow in finder. Put a copy of the Alfred.hs in it. Now, create the file cases.hs and place this code there:
module Main where import System.Environment import System.Exit import Data.List import Data.Char import qualified Data.ByteString import qualified Alfred as AL -- -- All of the words that are to be upper case only. -- upperWordsList = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "HTML", "CSS", "AT&T", "PHP", "UI"] -- -- All of the words that are to be lower case only. -- lowerWordsList = [ "to", "an", "and", "at", "as", "but", "by", "for", "if", "in", "on", "or", "is", "with", "a", "the", "of", "vs", "vs.", "via", "via", "en"] toWordLower :: String -> String toWordLower a = map toLower a toWordUpper :: String -> String toWordUpper a = map toUpper a toFirstLetterUpper :: [Char] -> [Char] toFirstLetterUpper  =  toFirstLetterUpper (a:) = toUpper a :  toFirstLetterUpper (a:ax) = toUpper a : toWordLower ax toFirstLetterUpperOnly :: [Char] -> [Char] toFirstLetterUpperOnly  =  toFirstLetterUpperOnly (a:) = toUpper a :  toFirstLetterUpperOnly (a:ax) = toUpper a : ax toFirstLetterLowerOnly :: [Char] -> [Char] toFirstLetterLowerOnly  =  toFirstLetterLowerOnly (a:) = toLower a :  toFirstLetterLowerOnly (a:ax) = toLower a : ax toTitleCase :: [Char] -> [Char] toTitleCase  =  toTitleCase a | elem (toWordUpper a) upperWordsList = toWordUpper a | elem (toWordLower a) lowerWordsList = toWordLower a | otherwise = toFirstLetterUpper a processLine :: String -> (String -> String) -> [String] -> String processLine "" f [a] = concatMap f $ words a processLine c f [a] = init $ concatMap (++ c) $ map f $ words a procLower :: [String] -> String procLower [a] = toWordLower a procUpper :: [String] -> String procUpper [a] = toWordUpper a procSentence :: [String] -> String procSentence [a] = toFirstLetterUpper a procTitle :: [String] -> String procTitle a = toFirstLetterUpperOnly $ processLine " " toTitleCase a procCamel :: [String] -> String procCamel a = toFirstLetterLowerOnly $ processLine "" toFirstLetterUpper a procSlash :: [String] -> String procSlash a = processLine "/" toWordLower a procPascal :: [String] -> String procPascal a = processLine "" toFirstLetterUpper a procCobra :: [String] -> String procCobra a = processLine "_" toFirstLetterUpper a procDot :: [String] -> String procDot a = processLine "." toWordLower a procDash :: [String] -> String procDash a = processLine "-" toWordLower a addItemCase :: String -> String -> String -> String addItemCase a b c = AL.addItemBasic a b b c alfredScript :: [String] -> IO () alfredScript  = putStr "" alfredScript [ caseStr ] = do putStr AL.begin putStr (addItemCase "HTCTitle" (procTitle [ caseStr ]) "Title Case") putStr (addItemCase "HTCLower" (procLower [ caseStr ]) "Lower Case") putStr (addItemCase "HTCUpper" (procUpper [ caseStr ]) "Upper Case") putStr (addItemCase "HTCSentenccaseStr" (procSentence [ caseStr ]) "Sentence Case") putStr (addItemCase "HTCCamel" (procCamel [ caseStr ]) "Camel Case") putStr (addItemCase "HTCSlash" (procSlash [ caseStr ]) "Slash Case") putStr (addItemCase "HTCPascal" (procPascal [ caseStr ]) "Pascal Case") putStr (addItemCase "HTCCobra" (procCobra [ caseStr ]) "Cobra Case") putStr (addItemCase "HTCDot" (procDot [ caseStr ]) "Dot Case") putStr (addItemCase "HTCDash" (procDash [ caseStr ]) "Dash Case") putStr AL.end main = do args <- getArgs alfredScript args
This code takes in a string from the command line and generates the Alfred Script Filter xml format with nine different case types.
The title case format uses two lists of words to make all upper case or all lower case. The actual function
toTitleCase uses conditionals to see if the word is in the upper case list or the lower case list. If it is, make it that way. Otherwise, just make the first letter upper case.
The helper function
processLine takes a string and a function, applies the function to all entries in a list of strings, adds the string to each entry, and concatenates the resulting strings together. This function processes many of the case conversions on a full string.
I created a helper function
addItemCase that duplicates the
arg value to the
title value in the
addItemBasic function in the Alfred library. Functional programming is about creating function of functions that get the job done without varialbles!
Most of the other functions are fairly easy to figure out. I give you the assignment to study this so that you can make your own Haskell workflow.
To compile the program, type this on the command line in the directory of the workflow:
With the program compiled and the workflow created, you can now use it to convert strings. In the Alfred Prompt, type
ht:conv this is a test .
The above picture shows the result. Select the one you want and it is placed in to the clipboard. The download file contains a copy of this workflow. I also have an expanded version available at Haskell Text Converter on Packal.org.
Also, I am constantly improving and adding to the Alfred Library in Haskell. You can always get the lastest version at Alfred Library in Haskell GitHub Repository.
In this tutorial I have shown you how to install Haskell on your Mac, create an Alfred library to help create new workflows, and made a sample workflow with the library. Now, it is your turn to make some great Alfred Workflows. Please share them in the comments below.