In this article, I want to debunk the myths about the complexity and narrow specialization of functional programming in general and Haskell in particular. I will try to make this article understandable even for people with a minimal view of Haskell. But first a small introduction.
I categorize myself as a lazy amateur photographer. I have a good “mirrorless SLR”, sometimes I am attacked by the desire to click something around me. However, I am lazy, and then I have no time or poking around in the resulting photo archive. As a rule, photos are viewed once or twice immediately after shooting by connecting the camera to a TV via an HDMI cable. Then the photos are sent to
non-existence directory ~ / Pictures / Photos / Unsorted and, as a rule, remain there forever. With various specials. I somehow did not make friends with the software, so this mess lasted for almost two years. And so, in the wake of exploring Haskell, I'm ripe for a solution.
Denial of responsibility
I do not claim to be a functional programming guru, I admit that the code I wrote is terrible (after all, I have been doing Haskell closely for a little more than two months in my spare time), and moreover, it may not work properly. The purpose of this article is to show that Haskell is really a general-purpose language that can be used not only for the nightmarish, most complicated mathematical calculations, but also for quite ordinary, everyday tasks. And great to deal with them.
I am aware of the existence of special programs that solve this problem, but I needed a simple, elementary utility that does exactly what I want. I am aware that this problem could be solved on any bash or perl in general, but this is the choice of the author, that is, me.
')
Before you start
I think this is the last part of the long introduction. First, of course it is desirable to have a basic knowledge of Haskell, otherwise a lot can be incomprehensible. It would be nice to have an idea about monads. However, I will try to explain the "subtle" points in the process of presentation, and I hope that with some effort everyone will understand this article.
Secondly, the necessary tools. Further, it is assumed that all events unfold on a Linux machine, but with minor changes, all this is true for Windows, and even more so for Mac OS X. In order to be able to build the resulting program, you need to do three things:
- Download and install the Haskell platform from the repository of your distribution;
- Install the Haskell EXIF ​​library:
sudo cabal install --global exif
; - install GTK + \ Glade and the corresponding Haskell
sudo cabal install --global glade
: sudo cabal install --global glade
.
Formulation of the problem
So, I needed the simplest utility for “packing” photos by date into the appropriate daddy, which can be invoked both for a freshly connected device, and independently for sorting an already existing archive with photos. In principle, everything, nothing complicated.
How are we going to decide
It's simple. To begin with, it is necessary to support two modes:
- “Automatic” - when the directory that you want to “scan” is passed as a command line argument;
- “Manual” - when a simple GUI front-end is launched, in which we independently choose the directory for processing.
Generally speaking, the GUI here is not at all mandatory, but I remind you that this is such a small learning example that will show in general and that Haskell's GUI is also never scary.
Further, regardless of how the catalog was slipped to us, we bypass this catalog (including bypassing all the subdirectories, because many cameras (if not all) carefully put the photos in the PHOTO daddy, and then in some 100PANA, 101PANA, and t . p.) and process each individual file.
Processing each individual file to madness is simple: we read the Exif data and, if there is any, copy it (the file with the photo) into the directory corresponding to the extracted date.
Whether we wanted it or not, but in the process of telling what our program should do, we described its three main
functions :
- run the program and check the arguments;
- directory traversal;
- processing a specific file.
Let's add some sources now.
Closer to the code
Lyrical digression. Haskell programs have a modular structure, just like in C, but of course without terrible header files. The exported (external) functions are specified in the module description, followed by imports of other modules and code. Haskell programs consist only of a description of data types and functions. Everything.
So let's start over. Namely, with the main function of the Main module (after all, the program starts with it). Here is the code for the entire module.
Main:
- module Main where
- import System (getArgs)
- import manager
- import GUI
- main :: IO ()
- main = getArgs >> = launch
- - there should be a single argument with the path to the photos, otherwise we run the gui
- launch :: [String] -> IO ()
- launch [x] = processDirectory x
- launch _ = startGUI
* This source code was highlighted with Source Code Highlighter .
I will dwell on the description of our first module in a little more detail, with an explanation of the main things. So, the first line is the module description. Next, import lines (from the System module, only the getArgs function is imported, which returns a list of command line arguments
without the name of the executable file). The Manager and GUI modules will be described later.
Next comes the description of the
main
function. The first line of the description is an optional type definition, generally speaking, it is not always necessary, in most cases the type of the function can be
deduced from the types of arguments supplied to it. Any function is described by a
system of equations , and the second line is the equation itself.
Lyrical digression. Formally, all functions can be divided into clean and "dirty", with side effects. I'll try to explain on the fingers.
A function is pure if, regardless of anything, with the same input data, it generates the same output data, while it never changes any system states and does not depend on them (whether the state of the file system or the presence of an open TCP connection). Take for example the addition function. It takes two arguments, and if both of these arguments are equal to number 2, then at the output we will always (absolutely always) get the number 4. And yes, the addition function of TCP does not open connections.
Functions with side effects - a separate conversation, they are just invented in order to designate the dependence of functions on any external manifestations by means of a model called the
input / output monad (IO monad) . For simplicity of perception at the moment we will think about monads only in the context of “input / output” (although in fact this concept is much broader). A function with side effects is a description of the sequence of
actions , as a result of which we will get some value-result. Example:
readFile :: FilePath -> IO (String)
is a function of reading a file along a specified path to this file. More precisely, it is not a function, it is its type. The type of functions is very simple to read, first comes the name of the function, then after two colons with the symbol -> the arguments of the functions are separated and after the last -> there comes the type of the return value. This is a primitive description, but it is still sufficient. That is, it is clear that the function takes a certain FilePath and returns
an I / O action , the execution of which will give the value String.
Here it is important to understand that the sequence of actions! = Sequence of calculations. In fact,
we generally do not know exactly in which sequence the calculations will actually take place; we determine only the order of the actions for which they stand. Therefore, the presence of such an abstraction as monads does not make Haskell imperative.
So back to our
main
. It takes no arguments and obviously returns an I / O action. The execution of this action is the execution of our entire program. It's funny, but in the whole received program, not
a single pure function is written, which is strikingly different from all the existing tutorials, however, this is just the problem solved by the program. The sequence of actions is determined by the functions
>>=
and
>>
, which take two arguments. In the first case, the result of performing the action on the left is passed as an argument to the function call on the right, and in the second, the action on the left is just silently executed, and then the action is performed on the right.
In this case, the execution of the action defined by the
getArgs
function results in the execution of a list of application string strings, which is passed to the
launch
function. The launch function is described below. It takes a list of strings as an argument, and it also returns an I / O action that returns no value. As I said before, functions in Haskell are described by systems of equations, which are graphically represented in the definition of launch.
Lyrical digression. Which equation will be selected for each particular function call is determined by the pattern matching procedure, that is, pattern matching. The function template is defined by the part of the equation defined on the left. The pattern matching procedure selects the controls in the order they appear in the code. A generic pattern denoting “anything” is a
_
character. In templates, you can (and should!) Decompose various data structures, for example, writing
foo ["a",""] = 0
means that if the foo function comes with a list of two lines "a" and "b", then it will return 0.
So, if a list from a single value of
x
comes into the
launch
function, we assume that this is the path to the directory and call the
processDirectory
function from the Manager module with an argument equal to
itself (note that there is no need to explicitly specify which module the function if at the same time there is no double interpretation, i.e. there is a function with one name in two modules). And if something else is different (note the
_
pattern), then we launch the GUI.
That's the whole module Main! We have just implemented the function # 1 of our program, we launched it!
Yes, I understand perfectly how terribly this looks to the uninitiated, but now look again at the source of the Main module. Reread all the lyrical digressions and tell yourself, is it so difficult? Maybe just unusual? For those who bravely and stoically endured all the tests, it is proposed to continue reading.
EXIF data
Let's leave for now the main computational process and write the function of extracting data about the time of the photograph from EXIF. For this, a separate Photo module was created. Why separate? Because it was dictated to me by my inner sense, it is assumed that this is a module for working with photography, what if my software grows into additional functionality? Let's look at its source.
- module Photo (
- getTime
- ) where
- import data . Time
- import System.Locale
- import Graphics.Exif
- getTime :: String -> IO (Maybe Day )
- getTime filePath =
- fromFile filePath >> = (\ e -> getTag e "DateTime") >> = ( return . parseDateTime)
- where
- parseDateTime (Just str) = parseTime defaultTimeLocale "% Y:% m:% d% H:% M:% S" str
- parseDateTime Nothing = Nothing
* This source code was highlighted with Source Code Highlighter .
Fearfully? Not at all. Note that this time in the definition of the module we indicated in brackets the name of the getTime function. Here are the functions that are accessible from outside the module. This is a simple and elegant solution instead of header header files that have passed in C \ C ++.
Pay attention to the getTime function. It takes as an argument the path to the file, and returns an action that will result in
Day . Maybe here is not some keyword, it is quite a certain type, which can take two values: Just Something or Nothing. This data type is used if, as a result, the function may not return any specific value from the value range. For example, searching for an item in the list that meets specified conditions. This element may not be.
Here, the presence of Maybe tells us that there may be no time data in this file. Day is just one of the data types representing time in Haskell. The definition of the getTime function (that is, the right side of the equation) immediately introduces two more concepts new to us. But first things first. It can be seen that getTime consists of three actions:
- first, the fromFile function from the Graphics.Exif module reads an object of Exif type, the execution of the corresponding action gives us this object and it is passed to the call of the next function;
- The following function is a lambda expression
\ e -> getTag e "DateTime"
, here the backslash imperceptibly resembles the Greek symbol "lambda", it is a function of one argument, which accepts the previously received Exif, which returns the IO action (Maybe String) returned the getTag function that accepts Exif and the tag name; - the result of the previous action is given to the final structure, we describe it in more detail.
First, the terrible and controversial word return. In fact, return in Haskell does not return anything, and the definition of the function is very indirect. return is a simple function that turns a value into
action into it. The point is that a function whose execution is an execution of a chain of actions cannot return a “pure” value. It cannot return “just a String”, because getting this String is possible only when performing a certain chain of actions, that is, getting this String in itself is
also an action , so we need to wrap the resulting value into a monad, i.e. “Return it” (return) to where it was received from. It was obtained from the monad, we need to return it (wrap?) To the monad again. As for the return value: the monadic function returns the last action defined in it, i.e. in this case, just return.
Secondly, the dot symbol is a composition of functions. Let
fy = z; gx = y;
fy = z; gx = y;
, then
(f . g) x == f( gx ) == z
. As a result of the second action, we get a
string with the date, and the function itself returns
Day . Therefore, we can be this line first parsim, as a result, obviously, it turns out there can be a date, we return it. In fact, the last action can be written as:
\ str -> return (parseDateTime str)
And finally, the where block, which explains that we also used parseDateTime in the definition of getTime. In the where block, the functions of the context are defined, let's say. Their scope is limited to the specific definition in which this block is found. In these context functions, you can and should refer to the input arguments of the parent function. In this case, such a function of the context is the function of parsing values. We carried it out in a separate block, because it must be represented by two equations (otherwise it would be possible to substitute its definition directly into the getTime definition) for two patterns — with Just and Nothing. In the first case, we parse the value, and in the second we return Nothing again. Agree, quite clearly?
With the function of extracting time is over. Prisutpim to bypass the directory.
Function # 2 - directory traversal
Lyrical digression. Haskell makes you think a little differently, not the way we used to solve problems in imperative languages. When it comes to, say, traversing a directory with files, a C programmer would say the following: “I’ll go through the files and check A, I’ll do it B”. However, a programmer in a functional language would say: “I will apply the processing function to each file in a directory”.
Slightly rebuilding your thinking you stop regretting the lost cycles. In the functional language, they are successfully replaced by convolutions and "maps" (map). Convolution we do not need here, I will not bother with explanations. I’ll dwell on the map function. The Russian word closest in meaning is “mapping,” and this is indeed so. The map function maps one data set to another. It takes as input a list of elements of type
a , a function of type
a -> b (but what, this is a functional language, here you can pass a function as an argument!) And we get a list of type b at the output. Here is the type of map function:
map :: (a -> b) -> [a] -> [b]
Note the parentheses of the first argument, they report that the second argument is a function in itself. Generally speaking, in Haskell, all functions are actually functions of one argument, just as a result, some of them return not values, but also functions. Consider the addition function:
add :: Int -> Int -> Int
Her type is absolutely correct to write this:
add :: (Int -> (Int -> Int))
Feel the difference? So,
add 5 2
returns the value 7. But
add 5
, returns a function that takes a value of type Int and returns a value of type Int! It is on such miracles that the principle of using the map function is built. This is how we add one to all elements of the source list:
map (+ 1) [1,2,3]
But you’ll find similar examples in any Haskell tutorial. Let's return to our sheep. Now it becomes clear that with the entire list of items in the catalog you need to do a completely uniform action and get a list of the results of these actions (in fact, it will be a little wrong, but it does not matter). Feel it? This is where the use of map is requested. But first we will decide on what needs to be done with each element of the catalog.
- first, you need to know the directory or file;
- secondly, if this is a directory, then we go inside, i.e. call the directory processing function again;
- Finally, thirdly, if this is a file, then you need to try to extract time from its EXIF ​​data, and if it is, copy it to the appropriate place.
Next, I will intentionally delete part of the original code (or replace it with “pseudo-code” stubs), leaving only the most important. Imagine that we somehow learned the information about whether a given entry is a directory while we omit it. Then consider the following processing function:
- - processing of the element: if the directory - enter recursion, otherwise we process the photo
- processSingle :: ( String , Bool) -> IO ()
- processSingle (path, True) = processDirectory path
- processSingle (path, False) = do
- picturesDir <- getPicturesDir
- maybeDate <- getTime path
- copyPhoto picturesDir maybeDate
- where
- - safe copying
- copyPhoto pictures Nothing = return ()
- copyPhoto pictures (Just date) = do
- let newPath = pictures ++ "/" ++ (formatTime defaultTimeLocale "% Y /% B /% d" date)
- copyFile path newPath
* This source code was highlighted with Source Code Highlighter .
So, we got the processSingle function. It takes as its argument a two-element tuple (that is, a pair of values): the path to the file system element and the sign that this is a directory. Taking advantage of the pattern matching, the function was divided into two equations: the first for the directory (going into recursion - the processDirectory function will be discussed later), the second for the file. Here we first learn about the do-action notation.
Lyrical digression. Up to this point, we recorded the sequence of actions “one after another”, separating them with special icons. This is not always convenient, for example, if you need to make a comparison with the template for the value extracted from the action “on the fly”. Or, what happens more often, you need to perform two actions, and transfer their result to the third function. Here do-notation comes to the rescue, which is actually more friendly to the eyes of beginners. Here are two equivalent codes:
- foobar = action1 >> = action2 >> = action3
- foobar '= do
- result1 <- action1
- result2 <- action2 result1
- action3 result2
* This source code was highlighted with Source Code Highlighter .
In this case, two results of two different actions are used as an argument to the third function. The copy function is trivial:
newPath
by pattern, if you find a date, then copy the file from
path
to
newPath
. Note two features:
- The path is an argument of the most "upper" function processSingle, i.e. copyPhoto is a context-sensitive function;
- let is a keyword of a language, it allows you to bind some value to a certain name (please note, this is not an assignment , its value cannot be changed, although this name can be bound to another value, but if you passed it to some function before then it will not change there) - in this case it is applied simply for convenience.
formatTime
is obviously a function that accepts a locale, a pattern, and a date, and returns a string with a date formatted with this pattern.
++
is a list concatenation function.
SUDDENLY, we just described feature # 3. Really simple? So what will our function number 2 look like? It will look like this:
- processDirectory :: String -> IO ()
- processDirectory dir =
- getDirectoryContents dir >> = checkItems >> = (mapM_ processSingle)
- where
- - according to the given list of contents of the catalog we return tuples with a marker "catalog" for each element
- checkItems xs = mapM singleCheck xs
- where
- singleCheck path = do
- isDirectory <- (doesDirectoryExist path)
- return (path, isDirectory)
* This source code was highlighted with Source Code Highlighter .
Take a closer look at this, in fact it is a very simple function. But first, a few words about mapM and mapM_.
Lyrical digression. The map function, which was discussed above, works only with pure code.
We have the same monadic code, so we must use a monadic map. Consider the types of these functions:mapM :: (a -> IO b) -> [a] -> IO [b]
mapM_ :: (a -> IO b) -> [a] -> IO ()
They have exactly the same meaning as in a clean map, but at the output they give not pure values, but actions that return these values. In the first case, we are interested in the result of processing each specific value and we get an action, as a result of which a list of processed values ​​is obtained. In the second case, we are not interested in the result of the processing, we need to just perform the actions.So, we will read the code from the bottom up:- The singleCheck function is a function that checks whether a directory is located along a given path and returns a pair of marker paths (this is exactly the construction that we have processed above!);
- The checkItems function takes a list of files (paths to them) and does not return an action that, when executed, will give a list of path-marker pairs (here we are interested in the result of the action, therefore mapM);
- Finally, the processDirectory function receives a list of files, sets a marker to each of them, and then processes each received pair according to the processSingle function defined above (here the processing itself does not return anything significant, we are not interested in the result of the action, we need it to be executed).
And it's all!
The end! The program is ready. Before the final part, I want to say a few words about the GUI.GUI in declarative languages
What is the difference between declarative and imperative languages? In imperative languages, you need to describe HOW to make calculations, in declarative ones, you need to describe WHAT you want to receive. The clearest example of a declarative language is SQL. Now let's think for a second about the graphical interface. In fact, it is necessary only declaratively (in accordance with the rules of a particular toolkit) to describe what we want to see, and immediately next to describe what an object should do when interacting with it.Now back to Haskell - as a functional declarative language, it miraculously falls on this structure, because, for example, to create a button click handler, it is enough to pass a certain handler function as an argument! And it is very natural. I use GTK to create Haskell interfaces. It is done this way: I draw molds in Glade, and in Haskell code I place handlers.Here is the simplified code for our task:
- prepareGUI mainWindow startButton fileChooser =
- do
- onDestroy mainWindow mainQuit
- onClicked startButton (processClick fileChooser)
- where
- processClick fileChooser = fileChooserGetFilename fileChooser >> = processDirectory
* This source code was highlighted with Source Code Highlighter .
I think he does not need comments.Conclusion
I posted the full source of the project on github (link below), I invite everyone interested to get acquainted, I provided them with quite detailed comments. Despite the impressive volume of the resulting article, I believe that those who carefully read it realized that this is not so complicated your Haskell, it is just a little different. It is based on other principles, but based on them it is possible to write code of any level, be it system or application programming. You can go even further: for a moment, imagine what a wonderful MVC web framework could be born on the basis of Haskell! In this tutorial, I only touched the tip of the iceberg, in fact, there is still something to learn - polymorphism, type classes, parallellism! .. I hope that at least someone with this article has ignited a spark of desire to learn this wonderful language.Project on githubHaskell