I'm approaching an intermediate level in Haskell, but I don't really see how writing a program in Haskell is any more like writing a language than writing a program in, say, python. Could you elaborate or point to some DSLs written on top of Haskell? I'm very curious.
Here is a fantastic article to get you started down this path. Note that the author, in his introduction, submits that the approach to FizzBuzz in this paper is a "somewhat tongue-in-cheek solution." Nevertheless, by applying this kind of thinking to what is, in some ways, a deceptively simple-looking problem, the paper serves as a great starting point for using DSLs as an approach to problem-solving.
There is another article about FizzBuzz I wrote roughly 5 years ago (forgive the pronouns, I need to rebuild the site and I'm procrastinating on the css).
This uses a slightly different set of abstractions with the same problem.
But perhaps more specifically, when you select a monad stack (group of effects) to compose to solve a problem you're building the features of the language and its effects. And if you use the "final tagless" or "Free" approaches you're doing that even more directly.
You can think a monad defines a DSL. Let's take for example the Rand[0] monad:
type Rand g = RandT g Identity
newtype RandT g m a = ...
-- with
runRandT :: (Monad m, RandomGen g) => RandT g m a -> g -> m (a, g)
newtype MyType = Rand StdGen
-- now you can define
intGreaterThan :: Integer -> MyType Integer
intGreaterThan = ...
randomName :: Integer -> MyType String
randomName length = ...
-- an finally convine the two above
nRandomNames :: Integer -> Integer -> MyType [String]
nRandomNames length n
| n <=0 = return []
| otherwise = do
tailNames <- nRandomNames (n-1)
headName <- randomName length
return (headName:tailNames)
These functions of type `...->MyType a` can be viewed as a DSL where the Random generator state is abstracted away.
#include <stdio.h>
int
main(int argc, char **argv)
{
double i, s;
s = 0;
for (i = 1; i < 100000000; i++)
s += 1/i;
printf("Almost infinity is %g\n", s);
}
Lennarts-Computer% gcc -O3 inf.c -o inf
Lennarts-Computer% time ./inf
Almost infinity is 18.9979
1.585u 0.009s 0:01.62 97.5% 0+0k 0+0io 0pf+0w
And now the Haskell code:
import BASIC
main = runBASIC' $ do
10 LET I =: 1
20 LET S =: 0
30 LET S =: S + 1/I
40 LET I =: I + 1
50 IF I <> 100000000 THEN 30
60 PRINT "Almost infinity is"
70 PRINT S
80 END
And running it:
Lennarts-Computer% ghc --make Main.hs
[4 of 4] Compiling Main ( Main.hs, Main.o )
Linking Main ...
Lennarts-Computer% ./Main
Almost infinity is
18.9979
CPU time: 1.57s
As you can see it's about the same time. In fact the assembly code for the loops look pretty much the same. Here's the Haskell one: