Need help to get started

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
4 messages Options
Reply | Threaded
Open this post in threaded view
|

Need help to get started

tech
hello,
I'm an industrial robot tech and the code I write seems to be composed
of 99% side-effects ;-)
That's the big dumb ones bolted to the floor that make cars and stuff.
Decades of imperative sequence coding seem to have fossilized something
in my brain.

Trying to teach myself Haskell and going nowhere very slowly.
Spent the last 8 weeks slowly plodding through "Real World Haskell" and
"Learn you a Haskell" with the intention of re-writing a bash script in
Haskell.
I've normally learned programming by writing something I need instead of
tutorials with which I seem to have an attention deficit problem.

I use Linux for work and have written a bash script that uses bc and
heredocs to write an .adoc file and then calls asciidoctor-pdf to
generate an invoice.
It's about 90 lines so I am not a scripting pro either.
So typing './invoice Wk38' rolls a file called Wk38-of-2020-Invoice.pdf
to send to clients.

It was while writing this script that I stumbled upon pandoc and then
Haskell.
And that's where the trouble started.
Tha abstraction and macro-approach of Haskell could provide a whole bag
of tools for maintaining code (text files) on lines of multiple robots.

About the existing script:
I write plain text files on my phone that look like this:

  14 Sep 0745 1600 Foo y
  15 Sep 0815 1500 Foo y
  16 Sep 0745 1400 Foo y
  17 Sep 0745 1430 Foo y
  18 Sep 0745 1400 Foo y

and this one is called Wk38 and the fields are: |Date|Time In|Time
Out|Proj|Lunch.
Then I email it to myself and run the script.

So I would be very grateful if someone could write some code for me and
put my bootstraps in my fumbling hands:

  Get the name of the Wkxx file from the command line when running a
Haskell standalone executable.
  fileName <- getArgs
  Load the lines of the file into a list of list of String[[]]. (or a
better way?)
  lines <- fmap Text.lines (Text.readFile fileName)
  Recurse through each line of the list(s) and:
  *:  Construct a date::String from the first two fields. (just a string,
does not need a real "Date")
  *:  Calc dailyElapsedTime::Double from difference between fields 4 and
3.
  *:  Subtract 0.5hr lunch from dailyElapsedTime if field 6 == "y"
  *:  Add dailyElapsedTime to accumlatedWeekTime::Double.
  *:  Charge = If accumlatedWeekTime < 40hrs then multiply by rate1
  *:           elseif accumlatedWeekTime > 40hrs then multiply by rate2
  *:  Append |Date|Time In|Time Out|Charge|Project| to adoc file.

If I could load it into ghci and step through it the lights may come on.
Thanks very much
Noodles
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: Need help to get started

Haskell - Haskell-Cafe mailing list
October 18, 2020 6:20 AM, [hidden email] wrote:

> About the existing script:
> I write plain text files on my phone that look like this:
>
> 14 Sep 0745 1600 Foo y
>
> and this one is called Wk38 and the fields are: |Date|Time In|Time
> Out|Proj|Lunch.
> Then I email it to myself and run the script.
>
> So I would be very grateful if someone could write some code for me and
> put my bootstraps in my fumbling hands:
>
> Get the name of the Wkxx file from the command line when running a
> Haskell standalone executable.
> fileName <- getArgs
> Load the lines of the file into a list of list of String[[]]. (or a
> better way?)
> lines <- fmap Text.lines (Text.readFile fileName)
> Recurse through each line of the list(s) and:
> *: Construct a date::String from the first two fields. (just a string,
> does not need a real "Date")
> *: Calc dailyElapsedTime::Double from difference between fields 4 and
> 3.
> *: Subtract 0.5hr lunch from dailyElapsedTime if field 6 == "y"
> *: Add dailyElapsedTime to accumlatedWeekTime::Double.
> *: Charge = If accumlatedWeekTime < 40hrs then multiply by rate1
> *: elseif accumlatedWeekTime > 40hrs then multiply by rate2
> *: Append |Date|Time In|Time Out|Charge|Project| to adoc file.

I would break this apart with a couple of additional types:

    data Entry = Entry
      { eDay :: Int
      , eMonth :: Int
      , eStart :: TimeOfDay
      , eEnd :: TimeOfDay
      , eProject :: Text
      , eLunched :: Bool
      } deriving (Eq, Show)
   
    data Charge = Charge
      { cDay :: Int
      , cMonth :: Int
      , cStart :: TimeOfDay
      , cEnd :: TimeOfDay
      , cHoursCharged :: Double
      , cProject :: Text
      } deriving (Eq, Show)

Then the problem breaks down into:

1. Get filename
2. Read text from file
3. Parse text into [Entry]
4. Convert [Entry] into [Charge]
5. Write [Charge] to other file

Maybe a sketch will unstick you:

1. You want `[fileName] <- getArgs` here, as `getArgs :: IO [String]` returns a list of arguments. Your program will fail unless you invoke it with exactly one argument but that's fine for initial testing.

2. lines <- fmap Text.lines (Text.readFile fileName) will give you access to `lines :: [Text]`, which you can pass into other functions. This means you're not trying to do everything inside `main`, and won't have values of type `IO whatever` flying around the rest of your program.

3. At this stage, we have a parsing problem. We want a function like `parseEntry :: Text -> Either Text Entry`, where the `Left` side would be an error message (if that line fails to parse), or the `Entry` describing that line. I can see a couple of ways to attack this:

   a) Use Text.words and continue with ad-hoc parsing. You may find yourself reinventing wheels that are in the library ecosystem, but for learning that might be fine?
   b) Use a library like megaparsec and write a full-blown parser. You will get more for free, but the learning curve may be steeper.

   I am inclined to recommend option (a), so try writing out a bunch of functions:

     - parseDay :: Text -> Either Text Int
     - parseMonth :: Text -> Either Text Int
     - parseTimeOfDay :: Text -> Either Text TimeOfDay
     - parseYN :: Text -> Either Text Bool
     - etc.

   Once you have applied each word from the input line to one of these functions, you will have a lot of `Either Text somePart` values. To combine them into an `Entry`, you'll want to use the `Applicative` typeclass, specifically the `(<$>)` and `(<*>)` operators. `Either e` has an `Applicative` instance for any `e`, so we can get:

     - Entry :: Int -> Int -> TimeOfDay -> TimeOfDay -> Text -> Bool -> Entry

     - (<$>) :: Functor f => (a -> b) -> f a -> f b  -- Infix alias for fmap (every `Applicative` is a `Functor`)
     - We use it here with the types `f ~ Either Text`, `a ~ Int`, `b ~ Int -> TimeOfDay -> TimeOfDay -> Text -> Bool -> Entry`:
     - Entry <$> parseDay dayText :: Either Text (Int -> TimeOfDay -> TimeOfDay -> Text -> Bool -> Entry)

     - (<*>) :: Applicative f => f (a -> b) -> f a -> f b
     - With the types `f ~ Either Text`, `a ~ Int`, `b ~ TimeOfDay -> TimeOfDay -> Text -> Bool -> Entry`:
     - Entry <$> parseDay dayText <*> parseMonth monthText :: Either Text (TimeOfDay -> TimeOfDay -> Text -> Bool -> Entry)

     - And so on, toward Entry <$> parseDay dayText <*> parseMonth monthText <*> ...etc... <*> parseYN lunchText

That gives you `parseEntry`, which parses one line into one `Entry`. We need to apply it over every line in the input list, and `traverse` is the tool for that:

    traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

As before, we'll use `Either Text` as our `Applicative`. Lists (i.e. the type constructor `[]`) have a `Traversable` instance, so we can specialise this to:

    traverse :: (Text -> Either Text Entry) -> [Text] -> Either Text [Entry]

This runs the parser on each line, and "combines" the results. Because we're using `Either Text` for our `Applicative`, the effect is to stop on the first parse error and report it.

You can then case-match on the result to see whether you have an error, or have parsed a [Entry] which you'll now convert into [Charge].

4. A function `calculateCharge :: Entry -> Charge` shouldn't be too difficult to write, and then you can lift it to work over lists using `map`. If you lean into the date/time types a bit more, you might find the `Data.Time.LocalTime.diffLocalTime` function (from the `time` package) helpful.

5. Appending `[Charge]` to the other file: There's an `appendFile :: FilePath -> Text -> IO ()` which should get you started. I'd look at writing a function `renderCharge :: Charge -> Text`, making it work over the entire list using `map`, and collapsing the `[Text]` using `Text.unlines`.

*****

Parsing looks like the gnarliest part, so I'd leave that to the end. Start at the outside and work your way in. Declare `parseLine` but replace its implementation with something silly:

    parseLine :: Text -> Either Text Entry
    parseLine _ = Right $ Entry 1 4 (TimeOfDay 9 0 0) (TimeOfDay 17 0 0) "Dummy" False

And see if you can get the rest of the program's skeleton in place. Then you can test that it does something, then replace `parseLine` with a real parser.

HTH,

-- Jack
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: Need help to get started

Henning Thielemann

On Sat, 17 Oct 2020, Jack Kelly via Haskell-Cafe wrote:

> 3. At this stage, we have a parsing problem. We want a function like `parseEntry :: Text -> Either Text Entry`, where the `Left` side would be an error message (if that line fails to parse), or the `Entry` describing that line. I can see a couple of ways to attack this:
>
>   a) Use Text.words and continue with ad-hoc parsing. You may find yourself reinventing wheels that are in the library ecosystem, but for learning that might be fine?
>   b) Use a library like megaparsec and write a full-blown parser. You will get more for free, but the learning curve may be steeper.

The text file looks like space-separated values. Thus, the cassava library
may help parsing stuff into the data type.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.
Reply | Threaded
Open this post in threaded view
|

Re: Need help to get started

tech
Thanks for all the input.
Honestly I was expecting to be curtly told to go and RTFM.
So very pleasantly surprised thank you.



On 2020-10-17 6:18 p.m., Henning Thielemann wrote:

>
> On Sat, 17 Oct 2020, Jack Kelly via Haskell-Cafe wrote:
>
>> 3. At this stage, we have a parsing problem. We want a function like
>> `parseEntry :: Text -> Either Text Entry`, where the `Left` side would
>> be an error message (if that line fails to parse), or the `Entry`
>> describing that line. I can see a couple of ways to attack this:
>>
>>   a) Use Text.words and continue with ad-hoc parsing. You may find
>> yourself reinventing wheels that are in the library ecosystem, but for
>> learning that might be fine?
>>   b) Use a library like megaparsec and write a full-blown parser. You
>> will get more for free, but the learning curve may be steeper.
>
> The text file looks like space-separated values. Thus, the cassava
> library may help parsing stuff into the data type.
_______________________________________________
Haskell-Cafe mailing list
To (un)subscribe, modify options or view archives go to:
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Only members subscribed via the mailman list are allowed to post.