diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 667fe64..80f64b0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: build: runs-on: ubuntu-latest container: - image: "docker://ghcr.io/chrisdone/hell-build@sha256:14776ba15fd7fce9ffff014ceb970a0e3ec2a1d12b601fcb47d7ff7010b7d7eb" + image: "docker://ghcr.io/chrisdone/hell-build@sha256:acfb500e0d1e2dd99a5c5b4a04ddad7021572032595c2a6a3dee866c32c27712" env: # For the ~/.stack root. diff --git a/docs/examples/index.html b/docs/examples/index.html new file mode 100644 index 0000000..950d0e1 --- /dev/null +++ b/docs/examples/index.html @@ -0,0 +1,386 @@ +
#!/usr/bin/env hell
+= Text.putStrLn "Hello, World!" main
= do
+ main "Please enter your name and hit ENTER:"
+ Text.putStrLn <- Text.getLine
+ name "Thanks, your name is: "
+ Text.putStrLn Text.putStrLn name
= do
+ main IO.hSetBuffering IO.stdin IO.NoBuffering
+ IO.hSetBuffering IO.stdout IO.NoBuffering
+
+"Please press any key ... "
+ Text.putStr <- ByteString.hGet IO.stdin 1
+ chunk
+IO.hSetBuffering IO.stdout IO.LineBuffering
+ "OK!" Text.putStrLn
= do
+ main let fp = "foo.txt"
+ "Hello, "
+ Text.writeFile fp "World!"
+ Text.appendFile fp <- Text.readFile fp
+ text Text.putStrLn text
= do
+ main let is = List.iterate' (Int.plus 1) 0
+ let xs = ["Hello, ", "World!"]
+ "OK!"
+ Text.putStrLn Monad.forM_ (List.zip is xs) \(i,x) -> do
+ IO.print i
+
+ Text.putStrLn xIO.print $ List.foldl' Int.plus 0 $ List.take 10 is
= do
+ main let x = "Hello!"
+
+ Text.putStrLn (Function.id x)let lengths = List.map Text.length ["foo", "mu"]
+ IO.mapM_ (\i -> Text.putStrLn (Int.show i)) lengths
= do
+ main IO.mapM_ Text.putStrLn ["Hello, ", "World!"]
+
+loop :: IO ()) -> do
+ Function.fix (\("Ahhhhh! More?"
+ Text.putStrLn <- Text.getLine
+ l loop)
= do
+ main let demo = \(x, y) -> y
+ let foobar = (123, "foo")
+
+ Text.putStrLn (demo foobar)
+let (foo,bar) = (123, "foo")
+ Text.putStrLn bar
= do
+ main "OK"
+ Text.putStrLn <- ByteString.readProcess (Process.proc "ls" ["-al"])
+ (code, out, err) ByteString.hPutStr IO.stdout out
+ ByteString.hPutStr IO.stdout err
+
+<- Text.readProcess_ (Process.proc "df" ["-h", "/"])
+ (out, err) IO.stdout out
+ Text.hPutStr IO.stdout err
+ Text.hPutStr
+<- Process.runProcess (Process.proc "false" [])
+ code
+"echo" ["Hello, World!"])
+ Process.runProcess_ (Process.proc
+-- Explicit type signature, but you don't need this. Just for demo'ing.
+ let config = Process.proc "false" []
+ <- Process.runProcess config
+ code "Done." Text.putStrLn
= do
+ main <- Directory.getCurrentDirectory
+ dir
+ Text.putStrLn dir Directory.setCurrentDirectory dir
= do
+ main <- Environment.getEnvironment
+ env <-
+ (out, err)
+ Text.readProcess_ ("HELL_DEMO", "wibble") env)
+ Process.setEnv (List.cons ("env" [])
+ (Process.proc
+ )IO.stdout out Text.hPutStr
= do
+ main Int.show (Main.fib 30))
+ Text.putStrLn (
+=
+ fib
+ Function.fix->
+ (\fib i Bool.bool
+ Bool.bool
+ (Int.plus (fib (Int.subtract 1 i))
+ (Int.subtract 2 i)))
+ (fib (1
+ Int.eq i 1))
+ (0
+ Int.eq i 0)
+ ( )
= do
+ main
+-- Run two things concurrently and return both results
+ <-
+ (left, right)
+ Async.concurrently"https://worldtimeapi.org/api/timezone/Europe/London")
+ (Main.curl "https://worldtimeapi.org/api/timezone/Europe/Rome")
+ (Main.curl
+ Text.putStrLn left
+ Text.putStrLn right
+-- Run two things concurrently and return the one that completes first
+ <-
+ result
+ Async.race"https://worldtimeapi.org/api/timezone/Europe/London")
+ (Main.curl "https://worldtimeapi.org/api/timezone/Europe/Rome")
+ (Main.curl Either.either Text.putStrLn Text.putStrLn result
+
+= \url -> do
+ curl <- Text.readProcess_ (Process.proc "curl" [url])
+ (out, err) IO.pure out
= do
+ main "Hello, ", "World!"])
+ Text.putStrLn (Text.concat [3 "Hello, World!")
+ Text.putStrLn (Text.take 3 "Hello, World!")
+ Text.putStrLn (Text.drop " Hello, World! ")
+ Text.putStrLn (Text.strip ", " ["Hello","World!"]) Text.putStrLn (Text.intercalate
= do
+ main Show.show 123)
+ Text.putStrLn (Show.show Bool.True)
+ Text.putStrLn (
+<- Environment.getEnvironment
+ env Maybe.maybe
+ "Seems the environment variable is not there.")
+ (Text.putStrLn -> Text.putStrLn (Text.concat ["HOME is ", path]))
+ (\path "HOME" env) (List.lookup
= do
+ main if List.and [Eq.eq (Int.plus 1 1) 2,
+ Ord.lt (Int.plus 1 1) 3,
+ Eq.eq (Text.concat ["Hello, World!"]) "Hello, World!"]
+ then Text.putStrLn "OK, List.and works."
+ else Text.putStrLn "Uh, oh?"
+
+if List.or [Eq.eq 1 2,
+ Eq.eq "x" "x"]
+ then Text.putStrLn "OK, List.or works."
+ else Text.putStrLn "Uh, oh?"
+
+if Bool.not (Eq.eq 1 2)
+ then Text.putStrLn "OK, Bool.not works."
+ else Text.putStrLn "Uh, oh?"
-- Technically you're not supposed to be able to do code re-use in
+-- Hell, but presently the desugarer inlines everything into `main`
+-- prior to type-checking, and ignores declarations that aren't
+-- reachable by main.
+
+= do
+ main 1
+ Main.foo "blah"
+ Main.foo = \x -> Text.putStrLn (Show.show x)
+ foo = Int.plus 4 "hi" bar
= do
+ main <- Environment.getEnvironment
+ env
+-- Maybe monad works!
+ Maybe.maybe (Text.putStrLn "Oops!") Text.putStrLn
+ do path <- List.lookup "PATH" env
+ (<- List.lookup "HOME" env
+ home Monad.return (Text.concat [path, " and ", home]))
+
+-- Either monad works!
+ Either.either Text.putStrLn Text.putStrLn
+ do x <- Main.parse "foo"
+ (<- Main.parse "foo"
+ y Monad.return (Text.concat [x,y]))
+
+= \s ->
+ parse if Eq.eq s "foo"
+ then Either.Right "foooo :-)"
+ else Either.Left "oh noes!"
-- This is a copy of the script that generates my blog.
+
+-- Dependencies:
+--
+-- hell-2024-02-07
+-- pandoc-3.1.11.1
+
+-- Main entry point just generates the complete blog every time.
+--
+--
+= Main.generate
+ main
+-- The posts are listed under ./posts in this format:
+--
+-- dijkstra-haskell-java.markdown
+-- reasoning-violently.md
+-- god-mode.markdown
+-- emacs-mail.markdown
+--
+-- .md or .markdown files, the extension doesn't matter.
+--
+= do
+ generate <- Main.generatePosts
+ posts
+ Main.generateArchive posts
+ Main.generateRSS posts
+-- Write out posts/$post/index.html per $post.
+--
+= do
+ generatePosts <- Directory.listDirectory "posts"
+ posts $ Text.concat ["Generating ", Show.show (List.length posts), " posts ..."]
+ Text.putStrLn -> do
+ Async.pooledForConcurrently posts \post <- Text.readFile $ Text.concat ["posts/", post]
+ contents Maybe.maybe
+ "Couldn't parse the article!")
+ (Error.error -> do
+ (\(date, title) <- Main.render post
+ rendered Monad.return (post, date, title, rendered))
+ $ Main.parse contents
+
+-- Generate the /posts/ page.
+--
+= \posts -> do
+ generateArchive "Generating archive ..."
+ Text.putStrLn let rows =
+
+ Text.concat$ List.map
+ ->
+ (\(post, date, title, content)
+ Text.concat ["<tr><td><a href='",
+
+ Main.filename post,"'>",
+
+ Main.strip title,"</td><td>",
+
+ date,"</td></tr>"
+
+ ])$ List.reverse
+ $ List.sortOn (\(post, date, title, content) -> date)
+ $ posts
+ let table = Text.concat [
+ "---\n",
+ "title: Archive\n",
+ "---\n",
+ "<table id='archive' style='line-height:2em'>",
+
+ rows,"</table>"
+
+ ]<-
+ (out, err)
+ Text.readProcess_$ Text.setStdin table
+ $ Process.proc "pandoc" ["--standalone","--template","templates/posts.html"]
+ "webroot/posts/index.html" out
+ Text.writeFile
+-- Contents of an article looks like this:
+--
+-- ---
+-- date: 2011-04-10
+-- title: ‘amb’ operator and the list monad
+-- description: ‘amb’ operator and the list monad
+-- author: Chris Done
+-- tags: haskell, designs
+-- ---
+--
+-- We're only interested in the date and the title. The rest is
+-- redundant.
+--
+= \article -> do
+ parse <- Text.stripPrefix "---" article
+ sansPrefix let (preamble, _content) = Text.breakOn "---" sansPrefix
+ let lines = Text.splitOn "\n" preamble
+ let pairs = List.map (\line -> do let (key, value) = Text.breakOn ":" line
+ 1 value)))
+ (key, Text.strip (Text.drop lines
+ <- List.lookup "date" pairs
+ date <- List.lookup "title" pairs
+ title Monad.return (date, title)
+
+-- A post consists of a date, title and markdown.
+--
+-- Rendering them is easy, just run pandoc and apply an HTML template.
+= \post -> do
+ render let targetDir =
+ "webroot/posts/", Main.filename post]
+ Text.concat [let targetFile = Text.concat [targetDir, "/index.html"]
+ <- Text.readProcess_ (Process.proc "pandoc" ["--standalone","--template","templates/post.html",Text.concat ["posts/", post]])
+ (out, err) Bool.True targetDir
+ Directory.createDirectoryIfMissing
+ Text.writeFile targetFile outMonad.return out
+
+-- Filename stripped of .md/.markdown.
+= \post -> Text.replace ".md" "" (Text.replace ".markdown" "" post)
+ filename
+-- Strip out quotes from "foo".
+= \title ->
+ strip Maybe.maybe title Function.id do
+ <- Text.stripPrefix "\"" title
+ title' "\"" title'
+ Text.stripSuffix
+-- Generate the /rss.xml page.
+--
+= \posts0 -> do
+ generateRSS let posts1 = List.reverse $ List.sortOn (\(post, date, title, content) -> date) posts0
+ <- Monad.forM posts1 \(post, date, title, content) -> do
+ posts <- Text.readProcessStdout_ $ Text.setStdin date $ Process.proc "date" ["-R", "-f", "/dev/stdin"]
+ date' Monad.return (post, date', title, content)
+ "Generating rss.xml ..."
+ Text.putStrLn let items =
+
+ Text.unlines$ List.map
+ ->
+ (\(post, date, title, content)
+ Text.concat ["<item>",
+ "<title><![CDATA[", Main.strip title, "]]></title>",
+ "<link>https://chrisdone.com/posts/", Main.filename post, "</link>",
+ "<guid>https://chrisdone.com/posts/", Main.filename post, "</guid>",
+ "<description><![CDATA[", content, "]]></description>",
+ "<pubDate>", date, "</pubDate>",
+ "<dc:creator>Chris Done</dc:creator>",
+ "</item>"
+
+ ])
+ postslet xml = Text.unlines [
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>",
+ "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">",
+ "<channel>",
+ "<title>Chris Done's Blog</title>",
+ "<link>https://chrisdone.com</link>",
+ "<description><![CDATA[Blog all about programming, especially in Haskell since 2008!]]></description>",
+ "<atom:link href=\"https://chrisdone.com/rss.xml\" rel=\"self\" type=\"application/rss+xml\" />",
+ "<lastBuildDate>Wed, 22 Dec 2021 00:00:00 UT</lastBuildDate>",
+
+ items,"</channel>",
+ "</rss>"
+
+ ]"webroot/rss.xml" xml Text.writeFile
= Text.putStrLn . Text.reverse $ "Foo!" main
= do
+ main ByteString.writeFile "demo.json" $
+ $
+ Json.encode Json.Object $ Map.fromList [
+ "name", Json.String "Chris"),
+ ("age", Json.Number 99.123)
+ (
+ ]<- ByteString.readFile "demo.json"
+ bytes ByteString.hPutStr IO.stdout bytes
+ $
+ Text.putStrLn Maybe.maybe "Bad parse."
+
+ (Json.value"null"
+ -> Text.concat ["bool", Show.show str])
+ (\str -> Text.concat ["str", Show.show str])
+ (\str -> Text.concat ["dub", Show.show dub])
+ (\dub -> "Array!")
+ (\arr -> "Object."))
+ (\obj $ Json.decode bytes
+ "demo.json" Directory.removeFile
data Person = Person { age :: Int, name :: Text }
+
+= do
+ main $ Record.get @"name" Main.person
+ Text.putStrLn $ Record.get @"name" $ Record.set @"name" "Mary" Main.person
+ Text.putStrLn $ Record.get @"name" $ Record.modify @"name" Text.reverse Main.person
+ Text.putStrLn
+=
+ person Main.Person { name = "Chris", age = 23 }
= do
+ main <- Environment.getArgs
+ args Monad.forM_ args IO.print
Hell is a shell scripting language that is a tiny dialect of Haskell that I wrote for my diff --git a/scripts/gen-docs.hell b/scripts/gen-docs.hell index 408c7ca..818a65d 100644 --- a/scripts/gen-docs.hell +++ b/scripts/gen-docs.hell @@ -4,4 +4,24 @@ main = do _out <- ByteString.readProcess_ (Text.setStdin script (Process.proc "stack" ["ghci","--no-load"])) + examples <- Directory.listDirectory "examples/" + let render = \fp -> do + Text.putStrLn $ Text.concat ["Rendering ", fp] + text <- Text.readFile fp + Text.readProcessStdout_ $ Text.setStdin (Text.unlines ["```haskell",text,"```"]) (Process.proc "pandoc" ["--from","markdown","--to","html"]) + frags <- Monad.forM (List.sort examples) \example -> do + out <- render $ Text.concat ["examples/", example] + Monad.return (example, out) + Text.writeFile "docs/examples/index.html" $ Text.concat [ + "", + "", + "
", + "", + "", + "", + "