Skip to content

Instantly share code, notes, and snippets.

@tfausak
Created November 22, 2020 15:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tfausak/eebeecf592c8f7ef8bbb576d85f4d616 to your computer and use it in GitHub Desktop.
Save tfausak/eebeecf592c8f7ef8bbb576d85f4d616 to your computer and use it in GitHub Desktop.
2020 State of Haskell Survey
{-# language OverloadedStrings #-}
module Main ( main ) where
import qualified Control.Monad as Monad
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Encode.Pretty as Aeson
import qualified Data.Aeson.Types as Aeson
import qualified Data.Bifunctor as Bifunctor
import qualified Data.ByteString.Lazy as LazyByteString
import qualified Data.Csv as Csv
import qualified Data.HashMap.Strict as HashMap
import qualified Data.List as List
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Ord as Ord
import qualified Data.Text as Text
import qualified Data.Time as Time
import qualified Data.Vector as Vector
import qualified Lucid
import qualified System.Directory as Directory
import qualified System.FilePath as FilePath
import qualified Text.Read as Read
main :: IO ()
main = do
putStrLn "Reading responses ..."
let directory = "s3" :: FilePath
files <- Directory.listDirectory directory
responses <- Monad.forM files $ \ file -> do
let path = FilePath.combine directory file
contents <- LazyByteString.readFile path
either fail pure $ Aeson.eitherDecode contents
putStrLn "Generating JSON ..."
LazyByteString.writeFile "2020-state-of-haskell-survey-results.json" $
Aeson.encodePretty responses
putStrLn "Generating CSV ..."
LazyByteString.writeFile "2020-state-of-haskell-survey-results.csv" $
Csv.encodeDefaultOrderedByName responses
putStrLn "Generating language extension CSV ..."
LazyByteString.writeFile "2020-haskell-language-extensions.csv"
. Csv.encodeDefaultOrderedByName
. fmap Extension
. Map.toAscList
. fmap (Bifunctor.bimap length length . List.partition id)
. Map.fromListWith (<>)
. fmap (\ (k, v) -> (k, [v]))
$ concatMap (Map.toList . responseS2Q5) responses
putStrLn "Generating HTML ..."
LazyByteString.writeFile "2020-state-of-haskell-survey-results.html"
. LazyByteString.concatMap (\ x -> LazyByteString.pack $ if x == 0x3c then [0x0a, x] else [x])
. Lucid.renderBS
. Lucid.doctypehtml_ $ do
Lucid.head_ $ do
Lucid.meta_ [Lucid.charset_ "utf-8"]
Lucid.title_ "2020 State of Haskell Survey Results"
Lucid.body_ $ do
Lucid.h1_ "2020 State of Haskell Survey Results"
Lucid.style_ $ Text.unlines
[ ".row { position: relative; }"
, ".bar { height: 100%; left: 0; max-width: 100%; position: absolute; top: 0; }"
, ".b1 { background-color: #9e9ac8; }" -- purple
, ".b2 { background-color: #cbc9e2; }" -- light purple
, ".b3 { background-color: #67a9cf; }" -- blue
, ".b4 { background-color: #ef8a62; }" -- red
, ".percent, .count, .choice { display: inline-block; position: relative; }"
, ".percent, .count { text-align: right; width: 3em; }"
, ".choice { padding-left: 1em; }"
]
Lucid.ol_
. Monad.forM_ sections
$ \ section -> Lucid.li_ $ do
Lucid.a_ [Lucid.href_ $ "#" <> sectionName section]
. Lucid.toHtml
$ sectionTitle section
Lucid.ol_
. Monad.forM_ (sectionQuestions section)
$ \ question -> Lucid.li_
. Lucid.a_ [Lucid.href_ $ "#" <> questionName section question]
. Lucid.toHtml
$ questionPrompt question
Monad.forM_ sections $ \ section -> do
Lucid.h2_ [Lucid.id_ $ sectionName section]
. Lucid.toHtml
$ sectionTitle section
Monad.forM_ (sectionQuestions section) $ \ question -> do
Lucid.h3_ [Lucid.id_ $ questionName section question]
. Lucid.toHtml
$ questionPrompt question
case questionExpect question of
ExpectSingle xs -> do
Lucid.p_ "Optional. Single select."
makeSingleChart xs (fetch section question) responses
ExpectMulti other xs -> do
let allowedOther = other == OtherAllow
Lucid.p_ $ do
"Optional. Multi select. "
if allowedOther
then "Allowed 'other'."
else "Did not allow 'other'."
let threshold = Time.UTCTime (Time.fromGregorian 2020 11 2) (Time.timeOfDayToTime $ Time.TimeOfDay 12 28 9)
makeMultiChart
(xs <> if allowedOther then ["Other"] else [])
(\ r ->
( responseFinish r > threshold
, fmap (\ x -> if elem x xs then x else "Other") $ fetch section question r
))
responses
ExpectEmail -> do
Lucid.p_ "Providing an email address was optional this year."
makeSingleChart
["Provided", "Not provided"]
(\ r -> [if null $ responseS0Q0 r then "Not provided" else "Provided"])
responses
ExpectLikert -> do
Lucid.p_ "Optional. Single select."
makeSingleChart
["Strongly disagree", "Disagree", "Neutral", "Agree", " Strongly agree"]
(fetch section question)
responses
ExpectFree -> Lucid.p_ "Optional. Free response answers were collected but not analyzed."
ExpectExtension xs -> do
Lucid.p_ "Optional. Multi select. Did not allow 'other'."
makeExtensionChart xs responses
putStrLn "Done."
newtype Extension
= Extension (Text.Text, (Int, Int))
deriving (Eq, Show)
instance Csv.DefaultOrdered Extension where
headerOrder = const $ Vector.fromList
[ "Name"
, "Pro"
, "Con"
]
instance Csv.ToNamedRecord Extension where
toNamedRecord (Extension (name, (pro, con))) = Csv.namedRecord
[ Csv.namedField "Name" name
, Csv.namedField "Pro" pro
, Csv.namedField "Con" con
]
fetch :: Section -> Question -> Response -> [Text.Text]
fetch section question = case (sectionIndex section, questionIndex question) of
-- (0, 0) -> responseS0Q0 -- email
(0, 1) -> responseS0Q1
(0, 2) -> responseS0Q2
(0, 3) -> responseS0Q3
(0, 4) -> responseS0Q4
(0, 5) -> responseS0Q5
(0, 6) -> responseS0Q6
(0, 7) -> responseS0Q7
(0, 8) -> responseS0Q8
(0, 9) -> responseS0Q9
(0, 10) -> responseS0Q10
(0, 11) -> responseS0Q11
(0, 12) -> responseS0Q12
(1, 0) -> responseS1Q0
(1, 1) -> responseS1Q1
(1, 2) -> responseS1Q2
(1, 3) -> responseS1Q3
(2, 0) -> responseS2Q0
(2, 1) -> responseS2Q1
(2, 2) -> responseS2Q2
(2, 3) -> responseS2Q3
(2, 4) -> responseS2Q4
-- (2, 5) -> responseS2Q5 -- extensions
(2, 6) -> responseS2Q6
(3, 0) -> responseS3Q0
(3, 1) -> responseS3Q1
(3, 2) -> responseS3Q2
(3, 3) -> responseS3Q3
(3, 4) -> responseS3Q4
(3, 5) -> responseS3Q5
(4, 0) -> responseS4Q0
(4, 1) -> responseS4Q1
(5, 0) -> responseS5Q0
(5, 1) -> responseS5Q1
(6, 0) -> responseS6Q0
(6, 1) -> responseS6Q1
(6, 2) -> responseS6Q2
(6, 3) -> responseS6Q3
(6, 4) -> responseS6Q4
(6, 5) -> responseS6Q5
(6, 6) -> responseS6Q6
(6, 7) -> responseS6Q7
(6, 8) -> responseS6Q8
(6, 9) -> responseS6Q9
(6, 10) -> responseS6Q10
(6, 11) -> responseS6Q11
(6, 12) -> responseS6Q12
(6, 13) -> responseS6Q13
(6, 14) -> responseS6Q14
(6, 15) -> responseS6Q15
(6, 16) -> responseS6Q16
(6, 17) -> responseS6Q17
(6, 18) -> responseS6Q18
(6, 19) -> responseS6Q19
(6, 20) -> responseS6Q20
(6, 21) -> responseS6Q21
(6, 22) -> responseS6Q22
(6, 23) -> responseS6Q23
(7, 0) -> responseS7Q0
(7, 1) -> responseS7Q1
(7, 2) -> responseS7Q2
(7, 3) -> responseS7Q3
(7, 4) -> responseS7Q4
(7, 5) -> responseS7Q5
(7, 6) -> responseS7Q6
(7, 7) -> responseS7Q7
(7, 8) -> responseS7Q8
(7, 9) -> responseS7Q9
(7, 10) -> responseS7Q10
(7, 11) -> responseS7Q11
(8, 0) -> responseS8Q0
(8, 1) -> responseS8Q1
t -> error $ show t
makeExtensionChart
:: [Text.Text]
-> [Response]
-> Lucid.Html ()
makeExtensionChart extensions responses = let
xs = filter (not . null) $ fmap responseS2Q5 responses
total = length xs
ys = Map.fromListWith (<>) $ concatMap (fmap (\ (k, v) -> (k, [v])) . Map.toList) xs
ts = fmap
(\ ext -> let
zs = Map.findWithDefault [] ext ys
ps = filter id zs -- votes "for" this extension
cs = filter not zs -- votes "against" this extension
cp = length ps -- count of pros
cc = length cs -- count of cons
pp = showText (round (100 * fromIntegral cp / fromIntegral total :: Double) :: Int) <> "%" -- percent of pros
pc = showText (round (100 * fromIntegral cc / fromIntegral total :: Double) :: Int) <> "%" -- percent of cons
in (ext, ((pp, pc), (cp, cc))))
extensions
in Lucid.div_ [Lucid.class_ "answer"]
. Monad.forM_ (List.sortOn (\ (_, (_, (cp, cc))) -> (Ord.Down cp, cc)) ts) $ \ (ext, ((pp, pc), (cp, cc))) ->
Lucid.div_ [Lucid.class_ "row"] $ do
Lucid.div_ [Lucid.class_ "bar b3", Lucid.style_ $ "width: " <> pp] mempty
Lucid.div_ [Lucid.class_ "bar b4", Lucid.style_ $ "left: " <> pp <> "; width: " <> pc] mempty
Lucid.div_ [Lucid.class_ "percent"] $ Lucid.toHtml $ "+" <> pp
Lucid.div_ [Lucid.class_ "percent"] . Lucid.toHtml $ "-" <> pc
Lucid.div_ [Lucid.class_ "count"] . Lucid.toHtml $ "+" <> show cp
Lucid.div_ [Lucid.class_ "count"] . Lucid.toHtml $ "-" <> show cc
Lucid.div_ [Lucid.class_ "choice"] $ Lucid.toHtml ext
makeMultiChart
:: [Text.Text]
-> (Response -> (Bool, [Text.Text]))
-> [Response]
-> Lucid.Html ()
makeMultiChart choices f responses = let
xs = filter (not . null . snd) $ fmap f responses
total = length xs
in Lucid.div_ [Lucid.class_ "answer"]
. Monad.forM_ choices $ \ choice -> Lucid.div_ [Lucid.class_ "row"] $ do
let
goods = filter (== choice) . concat . fmap snd $ filter fst xs
bads = filter (== choice) . concat . fmap snd $ filter (not . fst) xs
countGoods = length goods
countBads = length bads
percentGood = showText (round (100 * fromIntegral countGoods / fromIntegral total :: Double) :: Int) <> "%"
percentBad = showText (round (100 * fromIntegral countBads / fromIntegral total :: Double) :: Int) <> "%"
Lucid.div_ [Lucid.class_ "bar b1", Lucid.style_ $ "width: " <> percentGood] mempty
Lucid.div_ [Lucid.class_ "bar b2", Lucid.style_ $ "left: " <> percentGood <> "; width: " <> percentBad] mempty
Lucid.div_ [Lucid.class_ "percent"] $ Lucid.toHtml percentGood
Lucid.div_ [Lucid.class_ "percent"] . Lucid.toHtml $ "+" <> percentBad
Lucid.div_ [Lucid.class_ "count"] . Lucid.toHtml $ show countGoods
Lucid.div_ [Lucid.class_ "count"] . Lucid.toHtml $ "+" <> show countBads
Lucid.div_ [Lucid.class_ "choice"] $ Lucid.toHtml choice
makeSingleChart
:: [Text.Text]
-> (Response -> [Text.Text])
-> [Response]
-> Lucid.Html ()
makeSingleChart choices f responses = let
xs = filter (not . null) $ fmap f responses
total = length xs
in Lucid.div_ [Lucid.class_ "answer"]
. Monad.forM_ choices $ \ choice -> Lucid.div_ [Lucid.class_ "row"] $ do
let
ys = filter (== choice) $ concat xs
count = length ys
percent = showText (round (100 * fromIntegral count / fromIntegral total :: Double) :: Int) <> "%"
Lucid.div_ [Lucid.class_ "bar b1", Lucid.style_ $ "width: " <> percent] mempty
Lucid.div_ [Lucid.class_ "percent"] $ Lucid.toHtml percent
Lucid.div_ [Lucid.class_ "count"] . Lucid.toHtml $ show count
Lucid.div_ [Lucid.class_ "choice"] $ Lucid.toHtml choice
sectionName :: Section -> Text.Text
sectionName section = "s" <> showText (sectionIndex section)
questionName :: Section -> Question -> Text.Text
questionName section question = sectionName section
<> "q" <> showText (questionIndex question)
showText :: Show a => a -> Text.Text
showText = Text.pack . show
data Response = Response
{ responseStart :: Time.UTCTime
, responseFinish :: Time.UTCTime
, responseS0Q0 :: [Text.Text]
, responseS0Q1 :: [Text.Text]
, responseS0Q2 :: [Text.Text]
, responseS0Q3 :: [Text.Text]
, responseS0Q4 :: [Text.Text]
, responseS0Q5 :: [Text.Text]
, responseS0Q6 :: [Text.Text]
, responseS0Q7 :: [Text.Text]
, responseS0Q8 :: [Text.Text]
, responseS0Q9 :: [Text.Text]
, responseS0Q10 :: [Text.Text]
, responseS0Q11 :: [Text.Text]
, responseS0Q12 :: [Text.Text]
, responseS1Q0 :: [Text.Text]
, responseS1Q1 :: [Text.Text]
, responseS1Q2 :: [Text.Text]
, responseS1Q3 :: [Text.Text]
, responseS2Q0 :: [Text.Text]
, responseS2Q1 :: [Text.Text]
, responseS2Q2 :: [Text.Text]
, responseS2Q3 :: [Text.Text]
, responseS2Q4 :: [Text.Text]
, responseS2Q5 :: Map.Map Text.Text Bool
, responseS2Q6 :: [Text.Text]
, responseS3Q0 :: [Text.Text]
, responseS3Q1 :: [Text.Text]
, responseS3Q2 :: [Text.Text]
, responseS3Q3 :: [Text.Text]
, responseS3Q4 :: [Text.Text]
, responseS3Q5 :: [Text.Text]
, responseS4Q0 :: [Text.Text]
, responseS4Q1 :: [Text.Text]
, responseS5Q0 :: [Text.Text]
, responseS5Q1 :: [Text.Text]
, responseS6Q0 :: [Text.Text]
, responseS6Q1 :: [Text.Text]
, responseS6Q2 :: [Text.Text]
, responseS6Q3 :: [Text.Text]
, responseS6Q4 :: [Text.Text]
, responseS6Q5 :: [Text.Text]
, responseS6Q6 :: [Text.Text]
, responseS6Q7 :: [Text.Text]
, responseS6Q8 :: [Text.Text]
, responseS6Q9 :: [Text.Text]
, responseS6Q10 :: [Text.Text]
, responseS6Q11 :: [Text.Text]
, responseS6Q12 :: [Text.Text]
, responseS6Q13 :: [Text.Text]
, responseS6Q14 :: [Text.Text]
, responseS6Q15 :: [Text.Text]
, responseS6Q16 :: [Text.Text]
, responseS6Q17 :: [Text.Text]
, responseS6Q18 :: [Text.Text]
, responseS6Q19 :: [Text.Text]
, responseS6Q20 :: [Text.Text]
, responseS6Q21 :: [Text.Text]
, responseS6Q22 :: [Text.Text]
, responseS6Q23 :: [Text.Text]
, responseS7Q0 :: [Text.Text]
, responseS7Q1 :: [Text.Text]
, responseS7Q2 :: [Text.Text]
, responseS7Q3 :: [Text.Text]
, responseS7Q4 :: [Text.Text]
, responseS7Q5 :: [Text.Text]
, responseS7Q6 :: [Text.Text]
, responseS7Q7 :: [Text.Text]
, responseS7Q8 :: [Text.Text]
, responseS7Q9 :: [Text.Text]
, responseS7Q10 :: [Text.Text]
, responseS7Q11 :: [Text.Text]
, responseS8Q0 :: [Text.Text]
, responseS8Q1 :: [Text.Text]
} deriving (Eq, Show)
instance Aeson.FromJSON Response where
parseJSON = Aeson.withObject "Response" $ \ object -> let
req = fmap first . required object
opt = fmap (maybe [] unwrapList) . optional object
ext = pure . flip getExtensions object
in Response
<$> req "started-at"
<*> req "finished-at"
<*> opt "section-0-question-0"
<*> opt "section-0-question-1"
<*> opt "section-0-question-2"
<*> opt "section-0-question-3"
<*> opt "section-0-question-4"
<*> opt "section-0-question-5"
<*> opt "section-0-question-6"
<*> opt "section-0-question-7"
<*> opt "section-0-question-8"
<*> opt "section-0-question-9"
<*> opt "section-0-question-10"
<*> opt "section-0-question-11"
<*> opt "section-0-question-12"
<*> opt "section-1-question-0"
<*> opt "section-1-question-1"
<*> opt "section-1-question-2"
<*> opt "section-1-question-3"
<*> opt "section-2-question-0"
<*> opt "section-2-question-1"
<*> opt "section-2-question-2"
<*> opt "section-2-question-3"
<*> opt "section-2-question-4"
<*> ext "section-2-question-5"
<*> opt "section-2-question-6"
<*> opt "section-3-question-0"
<*> opt "section-3-question-1"
<*> opt "section-3-question-2"
<*> opt "section-3-question-3"
<*> opt "section-3-question-4"
<*> opt "section-3-question-5"
<*> opt "section-4-question-0"
<*> opt "section-4-question-1"
<*> opt "section-5-question-0"
<*> opt "section-5-question-1"
<*> opt "section-6-question-0"
<*> opt "section-6-question-1"
<*> opt "section-6-question-2"
<*> opt "section-6-question-3"
<*> opt "section-6-question-4"
<*> opt "section-6-question-5"
<*> opt "section-6-question-6"
<*> opt "section-6-question-7"
<*> opt "section-6-question-8"
<*> opt "section-6-question-9"
<*> opt "section-6-question-10"
<*> opt "section-6-question-11"
<*> opt "section-6-question-12"
<*> opt "section-6-question-13"
<*> opt "section-6-question-14"
<*> opt "section-6-question-15"
<*> opt "section-6-question-16"
<*> opt "section-6-question-17"
<*> opt "section-6-question-18"
<*> opt "section-6-question-19"
<*> opt "section-6-question-20"
<*> opt "section-6-question-21"
<*> opt "section-6-question-22"
<*> opt "section-6-question-23"
<*> opt "section-7-question-0"
<*> opt "section-7-question-1"
<*> opt "section-7-question-2"
<*> opt "section-7-question-3"
<*> opt "section-7-question-4"
<*> opt "section-7-question-5"
<*> opt "section-7-question-6"
<*> opt "section-7-question-7"
<*> opt "section-7-question-8"
<*> opt "section-7-question-9"
<*> opt "section-7-question-10"
<*> opt "section-7-question-11"
<*> opt "section-8-question-0"
<*> opt "section-8-question-1"
instance Aeson.ToJSON Response where
toJSON response = let
pair k f = k Aeson..= f response
in Aeson.object
[ pair "startedAt" responseStart
, pair "finishedAt" responseFinish
-- , pair "s0q0" responseS0Q0 -- email
, pair "s0q1" responseS0Q1
, pair "s0q2" responseS0Q2
, pair "s0q3" responseS0Q3
, pair "s0q4" responseS0Q4
, pair "s0q5" responseS0Q5
, pair "s0q6" responseS0Q6
, pair "s0q7" responseS0Q7
, pair "s0q8" responseS0Q8
, pair "s0q9" responseS0Q9
, pair "s0q10" responseS0Q10
, pair "s0q11" responseS0Q11
, pair "s0q12" responseS0Q12
, pair "s1q0" responseS1Q0
, pair "s1q1" responseS1Q1
, pair "s1q2" responseS1Q2
, pair "s1q3" responseS1Q3
, pair "s2q0" responseS2Q0
, pair "s2q1" responseS2Q1
, pair "s2q2" responseS2Q2
, pair "s2q3" responseS2Q3
, pair "s2q4" responseS2Q4
, pair "s2q5" responseS2Q5
, pair "s2q6" responseS2Q6
, pair "s3q0" responseS3Q0
, pair "s3q1" responseS3Q1
, pair "s3q2" responseS3Q2
, pair "s3q3" responseS3Q3
, pair "s3q4" responseS3Q4
, pair "s3q5" responseS3Q5
, pair "s4q0" responseS4Q0
, pair "s4q1" responseS4Q1
, pair "s5q0" responseS5Q0
, pair "s5q1" responseS5Q1
, pair "s6q0" responseS6Q0
, pair "s6q1" responseS6Q1
, pair "s6q2" responseS6Q2
, pair "s6q3" responseS6Q3
, pair "s6q4" responseS6Q4
, pair "s6q5" responseS6Q5
, pair "s6q6" responseS6Q6
, pair "s6q7" responseS6Q7
, pair "s6q8" responseS6Q8
, pair "s6q9" responseS6Q9
, pair "s6q10" responseS6Q10
, pair "s6q11" responseS6Q11
, pair "s6q12" responseS6Q12
, pair "s6q13" responseS6Q13
, pair "s6q14" responseS6Q14
, pair "s6q15" responseS6Q15
, pair "s6q16" responseS6Q16
, pair "s6q17" responseS6Q17
, pair "s6q18" responseS6Q18
, pair "s6q19" responseS6Q19
, pair "s6q20" responseS6Q20
, pair "s6q21" responseS6Q21
, pair "s6q22" responseS6Q22
, pair "s6q23" responseS6Q23
, pair "s7q0" responseS7Q0
, pair "s7q1" responseS7Q1
, pair "s7q2" responseS7Q2
, pair "s7q3" responseS7Q3
, pair "s7q4" responseS7Q4
, pair "s7q5" responseS7Q5
, pair "s7q6" responseS7Q6
, pair "s7q7" responseS7Q7
, pair "s7q8" responseS7Q8
, pair "s7q9" responseS7Q9
, pair "s7q10" responseS7Q10
, pair "s7q11" responseS7Q11
, pair "s8q0" responseS8Q0
, pair "s8q1" responseS8Q1
]
instance Csv.DefaultOrdered Response where
headerOrder = const $ Vector.fromList
[ "startedAt"
, "finishedAt"
-- , "s0q0" -- email
, "s0q1"
, "s0q2"
, "s0q3"
, "s0q4"
, "s0q5"
, "s0q6"
, "s0q7"
, "s0q8"
, "s0q9"
, "s0q10"
, "s0q11"
, "s0q12"
, "s1q0"
, "s1q1"
, "s1q2"
, "s1q3"
, "s2q0"
, "s2q1"
, "s2q2"
, "s2q3"
, "s2q4"
, "s2q5"
, "s2q6"
, "s3q0"
, "s3q1"
, "s3q2"
, "s3q3"
, "s3q4"
, "s3q5"
, "s4q0"
, "s4q1"
, "s5q0"
, "s5q1"
, "s6q0"
, "s6q1"
, "s6q2"
, "s6q3"
, "s6q4"
, "s6q5"
, "s6q6"
, "s6q7"
, "s6q8"
, "s6q9"
, "s6q10"
, "s6q11"
, "s6q12"
, "s6q13"
, "s6q14"
, "s6q15"
, "s6q16"
, "s6q17"
, "s6q18"
, "s6q19"
, "s6q20"
, "s6q21"
, "s6q22"
, "s6q23"
, "s7q0"
, "s7q1"
, "s7q2"
, "s7q3"
, "s7q4"
, "s7q5"
, "s7q6"
, "s7q7"
, "s7q8"
, "s7q9"
, "s7q10"
, "s7q11"
, "s8q0"
, "s8q1"
]
instance Csv.ToNamedRecord Response where
toNamedRecord response = let
field k f = Csv.namedField k $ f response
format = Time.formatTime Time.defaultTimeLocale "%Y-%m-%dT%H:%M:%S"
commas = Text.intercalate ","
in Csv.namedRecord
[ field "startedAt" $ format . responseStart
, field "finishedAt" $ format . responseFinish
-- , field "s0q0" responseS0Q0 -- email
, field "s0q1" $ commas . responseS0Q1
, field "s0q2" $ commas . responseS0Q2
, field "s0q3" $ commas . responseS0Q3
, field "s0q4" $ commas . responseS0Q4
, field "s0q5" $ commas . responseS0Q5
, field "s0q6" $ commas . responseS0Q6
, field "s0q7" $ commas . responseS0Q7
, field "s0q8" $ commas . responseS0Q8
, field "s0q9" $ commas . responseS0Q9
, field "s0q10" $ commas . responseS0Q10
, field "s0q11" $ commas . responseS0Q11
, field "s0q12" $ commas . responseS0Q12
, field "s1q0" $ commas . responseS1Q0
, field "s1q1" $ commas . responseS1Q1
, field "s1q2" $ commas . responseS1Q2
, field "s1q3" $ commas . responseS1Q3
, field "s2q0" $ commas . responseS2Q0
, field "s2q1" $ commas . responseS2Q1
, field "s2q2" $ commas . responseS2Q2
, field "s2q3" $ commas . responseS2Q3
, field "s2q4" $ commas . responseS2Q4
, field "s2q5" $ commas . fmap (\ (k, v) -> (if v then "+" else "-") <> k) . Map.toAscList . responseS2Q5
, field "s2q6" $ commas . responseS2Q6
, field "s3q0" $ commas . responseS3Q0
, field "s3q1" $ commas . responseS3Q1
, field "s3q2" $ commas . responseS3Q2
, field "s3q3" $ commas . responseS3Q3
, field "s3q4" $ commas . responseS3Q4
, field "s3q5" $ commas . responseS3Q5
, field "s4q0" $ commas . responseS4Q0
, field "s4q1" $ commas . responseS4Q1
, field "s5q0" $ commas . responseS5Q0
, field "s5q1" $ commas . responseS5Q1
, field "s6q0" $ commas . responseS6Q0
, field "s6q1" $ commas . responseS6Q1
, field "s6q2" $ commas . responseS6Q2
, field "s6q3" $ commas . responseS6Q3
, field "s6q4" $ commas . responseS6Q4
, field "s6q5" $ commas . responseS6Q5
, field "s6q6" $ commas . responseS6Q6
, field "s6q7" $ commas . responseS6Q7
, field "s6q8" $ commas . responseS6Q8
, field "s6q9" $ commas . responseS6Q9
, field "s6q10" $ commas . responseS6Q10
, field "s6q11" $ commas . responseS6Q11
, field "s6q12" $ commas . responseS6Q12
, field "s6q13" $ commas . responseS6Q13
, field "s6q14" $ commas . responseS6Q14
, field "s6q15" $ commas . responseS6Q15
, field "s6q16" $ commas . responseS6Q16
, field "s6q17" $ commas . responseS6Q17
, field "s6q18" $ commas . responseS6Q18
, field "s6q19" $ commas . responseS6Q19
, field "s6q20" $ commas . responseS6Q20
, field "s6q21" $ commas . responseS6Q21
, field "s6q22" $ commas . responseS6Q22
, field "s6q23" $ commas . responseS6Q23
, field "s7q0" $ commas . responseS7Q0
, field "s7q1" $ commas . responseS7Q1
, field "s7q2" $ commas . responseS7Q2
, field "s7q3" $ commas . responseS7Q3
, field "s7q4" $ commas . responseS7Q4
, field "s7q5" $ commas . responseS7Q5
, field "s7q6" $ commas . responseS7Q6
, field "s7q7" $ commas . responseS7Q7
, field "s7q8" $ commas . responseS7Q8
, field "s7q9" $ commas . responseS7Q9
, field "s7q10" $ commas . responseS7Q10
, field "s7q11" $ commas . responseS7Q11
, field "s8q0" $ commas . responseS8Q0
, field "s8q1" $ commas . responseS8Q1
]
required :: Aeson.FromJSON a => Aeson.Object -> Text.Text -> Aeson.Parser a
required object key = object Aeson..: key
optional :: Aeson.FromJSON a => Aeson.Object -> Text.Text -> Aeson.Parser (Maybe a)
optional object key = object Aeson..:? key
-- The extension answers look like this:
--
-- > { "section-2-question-5-choice-0-value": "OverloadedStrings"
-- > , "section-2-question-5-choice-0": "yes"
-- > , ... }
--
-- This function is responsible for crunching that into this:
--
-- > { "OverloadedStrings": true
-- > , ... }
getExtensions :: Text.Text -> Aeson.Object -> Map.Map Text.Text Bool
getExtensions prefix
= Map.fromList
. Maybe.mapMaybe (\ xs -> let
(maybeExtension, maybeWanted) = foldr
(\ e (ml, mr) -> case e of
Left l -> (Just l, mr)
Right r -> (ml, Just r))
(Nothing, Nothing)
xs
in (,) <$> maybeExtension <*> maybeWanted)
. Map.elems
. Map.fromListWith (<>)
. (\ xs -> xs :: [(Int, [Either Text.Text Bool])])
. Maybe.mapMaybe (\ (key, value) -> do
Monad.guard . not $ Text.isSuffixOf "-time" key
noPrefix <- Text.stripPrefix (prefix <> "-choice-") key
case Text.stripSuffix "-value" noPrefix of
Nothing -> do
index <- Read.readMaybe $ Text.unpack noPrefix
case Aeson.fromJSON value of
Aeson.Error _ -> Nothing
Aeson.Success xs -> pure (index, fmap (Right . isYes) $ unwrapList xs)
Just noSuffix -> do
index <- Read.readMaybe $ Text.unpack noSuffix
case Aeson.fromJSON value of
Aeson.Error _ -> Nothing
Aeson.Success xs -> pure (index, fmap Left $ unwrapList xs))
. HashMap.toList
isYes :: Text.Text -> Bool
isYes = (==) "yes"
data List a
= One a
| Many (NonEmpty.NonEmpty a)
deriving (Eq, Show)
instance Aeson.FromJSON a => Aeson.FromJSON (List a) where
parseJSON value = case value of
Aeson.Array _ -> fmap Many $ Aeson.parseJSON value
_ -> fmap One $ Aeson.parseJSON value
unwrapList :: List a -> [a]
unwrapList list = case list of
One x -> [x]
Many xs -> NonEmpty.toList xs
first :: List a -> a
first list = case list of
One x -> x
Many xs -> NonEmpty.head xs
newtype Number
= Number Double
deriving (Eq, Show)
instance Aeson.FromJSON Number where
parseJSON = Aeson.withText "Number" $
either fail (pure . Number) . Read.readEither . Text.unpack
data Section = Section
{ sectionIndex :: Int
, sectionTitle :: Text.Text
, sectionQuestions :: [Question]
} deriving (Eq, Show)
data Question = Question
{ questionIndex :: Int
, questionPrompt :: Text.Text
, questionExpect :: Expect
} deriving (Eq, Show)
data Expect
= ExpectEmail
| ExpectSingle [Text.Text]
| ExpectLikert
| ExpectMulti Other [Text.Text]
| ExpectExtension [Text.Text]
| ExpectFree
deriving (Eq, Show)
data Other
= OtherAllow
| OtherReject
deriving (Eq, Show)
sections :: [Section]
sections =
[ Section 0 "Haskell usage"
[ Question 0 "What is your email address?" ExpectEmail
, Question 1 "Do you use Haskell?" $ ExpectSingle
[ "Yes"
, "No, but I used to"
, "No, I never have"
]
, Question 2 "If you stopped using Haskell, how long did you use it before you stopped?" $ ExpectSingle
[ "Less than 1 day"
, "1 day to 1 week"
, "1 week to 1 month"
, "1 month to 1 year"
, "More than 1 year"
, "n/a"
]
, Question 3 "If you do not use Haskell, why not?" $ ExpectMulti OtherAllow
[ "Haskell does not support the platforms I need"
, "Haskell is too hard to learn"
, "Haskell lacks critical features"
, "Haskell lacks critical libraries"
, "Haskell lacks critical tools"
, "Haskell's documentation is not good enough"
, "Haskell's performance is not good enough"
, "My company doesn't use Haskell"
]
, Question 4 "How long have you been using Haskell?" $ ExpectSingle
[ "Less than 1 day"
, "1 day to 1 week"
, "1 week to 1 month"
, "1 month to 1 year"
, "1 year to 2 years"
, "2 years to 3 years"
, "3 years to 4 years"
, "4 years to 5 years"
, "5 years to 6 years"
, "6 years to 7 years"
, "7 years to 8 years"
, "8 years to 9 years"
, "9 years to 10 years"
, "10 years to 15 years"
, "15 years to 20 years"
, "More than 20 years"
]
, Question 5 "How frequently do you use Haskell?" $ ExpectSingle
[ "Daily"
, "Weekly"
, "Monthly"
, "Yearly"
, "Rarely"
]
, Question 6 "How would you rate your proficiency in Haskell?" $ ExpectSingle
[ "Beginner"
, "Intermediate"
, "Advanced"
, "Expert"
, "Master"
]
, Question 7 "Where do you use Haskell?" $ ExpectMulti OtherReject
[ "Academia"
, "Home"
, "Industry"
, "School"
]
, Question 8 "Do you use Haskell at work?" $ ExpectSingle
[ "Yes, most of the time"
, "Yes, some of the time"
, "No, but my company does"
, "No, but I'd like to"
, "No, and I don't want to"
]
, Question 9 "If you do not use Haskell at work, why not?" $ ExpectMulti OtherAllow
[ "Haskell does not support the platforms I need"
, "Haskell is too hard to learn"
, "Haskell lacks critical features"
, "Haskell lacks critical libraries"
, "Haskell lacks critical tools"
, "Haskell's documentation is not good enough"
, "Haskell's performance is not good enough"
, "It's too hard to hire Haskell developers"
, "My company doesn't use Haskell"
]
, Question 10 "Which programming languages other than Haskell are you fluent in?" $ ExpectMulti OtherAllow
[ "Assembly"
, "C"
, "C#"
, "C++"
, "Clojure"
, "Elm"
, "Erlang"
, "F#"
, "Go"
, "Groovy"
, "Hack"
, "Java"
, "JavaScript"
, "Julia"
, "Kotlin"
, "Lua"
, "Matlab"
, "Objective-C"
, "Ocaml"
, "Perl"
, "PHP"
, "PureScript"
, "Python"
, "R"
, "Ruby"
, "Rust"
, "Scala"
, "Shell"
, "Swift"
, "TypeScript"
, "VBA"
, "VB.NET"
]
, Question 11 "Which types of software do you develop with Haskell?" $ ExpectMulti OtherAllow
[ "Agents or daemons"
, "API services (returning non-HTML)"
, "Automation or scripts"
, "Command-line programs (CLI)"
, "Data processing"
, "Desktop programs (GUI)"
, "Libraries or frameworks"
, "Web services (returning HTML)"
]
, Question 12 "Which industries do you use Haskell in?" $ ExpectMulti OtherAllow
[ "Academia"
, "Banking or finance"
, "Commerce or retail"
, "Cryptocurrency"
, "Education"
, "Embedded"
, "Gaming"
, "Government"
, "Healthcare or medical"
, "Mobile"
, "Science"
, "Web"
]
]
, Section 1 "Projects"
[ Question 0 "How many Haskell projects do you contribute to?" $ ExpectSingle
[ "0"
, "1"
, "2"
, "3"
, "4"
, "5"
, "6 to 10"
, "11 to 20"
, "More than 20"
]
, Question 1 "What is the total size of all the Haskell projects you contribute to?" $ ExpectSingle
[ "Less than 1,000 lines of code"
, "Between 1,000 and 9,999 lines of code"
, "Between 10,000 and 99,999 lines of code"
, "More than 100,000 lines of code"
]
, Question 2 "Which platforms do you develop Haskell on?" $ ExpectMulti OtherAllow
[ "BSD"
, "Linux"
, "MacOS"
, "Windows"
]
, Question 3 "Which platforms do you target?" $ ExpectMulti OtherAllow
[ "Android"
, "BSD"
, "iOS"
, "Linux"
, "MacOS"
, "Windows"
]
]
, Section 2 "Compilers"
[ Question 0 "Which Haskell compilers do you use?" $ ExpectMulti OtherAllow
[ "GHC"
, "GHCJS"
]
, Question 1 "Which installation methods do you use for your Haskell compiler?" $ ExpectMulti OtherAllow
[ "Source"
, "Official binaries"
, "Minimal installer"
, "Haskell Platform"
, "Operating system package"
, "Stack"
, "Nix"
, "ghcup"
]
, Question 2 "Has upgrading your Haskell compiler broken your code in the last year?" $ ExpectSingle
[ "Yes"
, "No"
]
, Question 3 "How has upgrading your Haskell compiler broken your code in the last year?" $ ExpectMulti OtherAllow
[ "Compiler bugs"
, "Expected changes, such as the MonadFail proposal"
, "Incompatible dependencies"
, "New warnings"
, "Unexpected changes"
]
, Question 4 "Which versions of GHC do you use?" $ ExpectMulti OtherAllow
[ "> 8.10"
, "8.10.x"
, "8.8.x"
, "8.6.x"
, "8.4.x"
, "8.2.x"
, "8.0.x"
, "< 8.0"
]
, Question 5 "Which language extensions would you like to be enabled by default?" $ ExpectExtension
[ "AllowAmbiguousTypes"
, "ApplicativeDo"
, "Arrows"
, "BangPatterns"
, "BinaryLiterals"
, "BlockArguments"
, "CApiFFI"
, "ConstrainedClassMethods"
, "ConstraintKinds"
, "Cpp"
, "DataKinds"
, "DatatypeContexts"
, "DefaultSignatures"
, "DeriveAnyClass"
, "DeriveDataTypeable"
, "DeriveFoldable"
, "DeriveFunctor"
, "DeriveGeneric"
, "DeriveLift"
, "DeriveTraversable"
, "DerivingStrategies"
, "DerivingVia"
, "DisambiguateRecordFields"
, "DuplicateRecordFields"
, "EmptyCase"
, "ExistentialQuantification"
, "ExplicitForAll"
, "ExplicitNamespaces"
, "ExtendedDefaultRules"
, "FlexibleContexts"
, "FlexibleInstances"
, "ForeignFunctionInterface"
, "FunctionalDependencies"
, "GADTs"
, "GADTSyntax"
, "GeneralizedNewtypeDeriving"
, "HexFloatLiterals"
, "ImplicitParams"
, "ImportQualifiedPost"
, "ImpredicativeTypes"
, "IncoherentInstances"
, "InstanceSigs"
, "InterruptibleFFI"
, "KindSignatures"
, "LambdaCase"
, "LiberalTypeSynonyms"
, "MagicHash"
, "MonadComprehensions"
, "MonoLocalBinds"
, "MultiParamTypeClasses"
, "MultiWayIf"
, "NamedFieldPuns"
, "NamedWildCards"
, "NegativeLiterals"
, "NoEmptyDataDecls"
, "NoImplicitPrelude"
, "NoMonadFailDesugaring"
, "NoMonomorphismRestriction"
, "NoPatternGuards"
, "NoStarIsType"
, "NoTraditionalRecordSyntax"
, "NPlusKPatterns"
, "NullaryTypeClasses"
, "NumDecimals"
, "NumericUnderscores"
, "OverlappingInstances"
, "OverloadedLabels"
, "OverloadedLists"
, "OverloadedStrings"
, "PackageImports"
, "ParallelListComp"
, "PartialTypeSignatures"
, "PatternSynonyms"
, "PolyKinds"
, "PostfixOperators"
, "QuantifiedConstraints"
, "QuasiQuotes"
, "Rank2Types"
, "RankNTypes"
, "RebindableSyntax"
, "RecordWildCards"
, "RecursiveDo"
, "RoleAnnotations"
, "ScopedTypeVariables"
, "StandaloneDeriving"
, "StandaloneKindSignatures"
, "StaticPointers"
, "Strict"
, "StrictData"
, "TemplateHaskell"
, "TemplateHaskellQuotes"
, "TransformListComp"
, "Trustworthy"
, "TupleSections"
, "TypeApplications"
, "TypeFamilies"
, "TypeFamilyDependencies"
, "TypeInType"
, "TypeOperators"
, "TypeSynonymInstances"
, "UnboxedSums"
, "UnboxedTuples"
, "UndecidableInstances"
, "UndecidableSuperClasses"
, "UnicodeSyntax"
, "UnliftedNewtypes"
, "Unsafe"
, "ViewPatterns"
]
, Question 6 "How important do you feel it would be to have a new version of the Haskell language standard?" $ ExpectSingle
[ "Extremely important"
, "Very important"
, "Moderately important"
, "Slightly important"
, "Not at all important"
]
]
, Section 3 "Tooling"
[ Question 0 "Which build tools do you use for Haskell?" $ ExpectMulti OtherAllow
[ "Bazel"
, "Cabal"
, "ghc-pkg"
, "Mafia"
, "Make"
, "Nix"
, "Shake"
, "Stack"
]
, Question 1 "Which editors do you use for Haskell?" $ ExpectMulti OtherAllow
[ "Atom"
, "Emacs family"
, "IntelliJ IDEA"
, "Notepad++"
, "Sublime Text"
, "Vi family"
, "Visual Studio Code"
]
, Question 2 "Which version control systems do you use for Haskell?" $ ExpectMulti OtherAllow
[ "Darcs"
, "Git"
, "Mercurial"
, "Pijul"
, "Subversion"
]
, Question 3 "Where do you get Haskell packages from?" $ ExpectMulti OtherAllow
[ "Hackage"
, "Nix"
, "Source"
, "Stackage"
]
, Question 4 "Which tools do you use to test Haskell code?" $ ExpectMulti OtherAllow
[ "Haskell Test Framework"
, "Hedgehog"
, "Hspec"
, "HUnit"
, "QuickCheck"
, "SmallCheck"
, "Tasty"
]
, Question 5 "Which tools do you use to benchmark Haskell code?" $ ExpectMulti OtherAllow
[ "Bench"
, "Criterion"
, "Gauge"
]
]
, Section 4 "Infrastructure"
[ Question 0 "Which tools do you use to deploy Haskell applications?" $ ExpectMulti OtherAllow
[ "Docker images"
, "Dynamic binaries"
, "Nix expressions"
, "Static binaries"
]
, Question 1 "Where do you deploy Haskell applications?" $ ExpectMulti OtherAllow
[ "Amazon Web Services"
, "Digital Ocean"
, "Google Cloud"
, "Heroku"
, "Microsoft Azure"
, "Self or company owned servers"
]
]
, Section 5 "Community"
[ Question 0 "Where do you interact with the Haskell community?" $ ExpectMulti OtherAllow
[ "Conferences (academic)"
, "Conferences (commercial)"
, "Discord"
, "Discourse"
, "GitHub"
, "Gitter"
, "IRC"
, "Lobsters"
, "Mailing lists"
, "Mastodon"
, "Matrix/Riot"
, "Meetups"
, "Reddit"
, "Slack"
, "Stack Overflow"
, "Telegram"
, "Twitter"
]
, Question 1 "Which of the following Haskell topics would you like to see more written about?" $ ExpectMulti OtherAllow
[ "Algorithm implementations"
, "Application architectures"
, "Beginner fundamentals"
, "Best practices"
, "Case studies"
, "Comparisons to other languages"
, "Debugging how-tos"
, "Design patterns"
, "Game development"
, "GUIs"
, "Library walkthroughs"
, "Machine learning"
, "Mobile development"
, "Performance analysis"
, "Production infrastructure"
, "Project maintenance"
, "Project setup"
, "Tooling choices"
, "Web development"
]
]
, Section 6 "Feelings"
[ Question 0 "I feel welcome in the Haskell community." ExpectLikert
, Question 1 "I am satisfied with Haskell as a language." ExpectLikert
, Question 2 "I am satisfied with Haskell's compilers, such as GHC." ExpectLikert
, Question 3 "I am satisfied with Haskell's build tools, such as Cabal." ExpectLikert
, Question 4 "I am satisfied with Haskell's package repositories, such as Hackage." ExpectLikert
, Question 5 "I can find Haskell libraries for the things that I need." ExpectLikert
, Question 6 "I think Haskell libraries are high quality." ExpectLikert
, Question 7 "I have a good understanding of Haskell best practices." ExpectLikert
, Question 8 "I think Haskell libraries are well documented." ExpectLikert
, Question 9 "I can easily compare competing Haskell libraries to select the best one." ExpectLikert
, Question 10 "I think that Haskell libraries are easy to use." ExpectLikert
, Question 11 "I think that Haskell libraries provide a stable API." ExpectLikert
, Question 12 "I think that Haskell libraries work well together." ExpectLikert
, Question 13 "I think that software written in Haskell is easy to maintain." ExpectLikert
, Question 14 "Once my Haskell program compiles, it generally does what I intended." ExpectLikert
, Question 15 "I think that Haskell libraries perform well." ExpectLikert
, Question 16 "Haskell's performance meets my needs." ExpectLikert
, Question 17 "I can easily reason about the performance of my Haskell code." ExpectLikert
, Question 18 "I would recommend using Haskell to others." ExpectLikert
, Question 19 "I would prefer to use Haskell for my next new project." ExpectLikert
, Question 20 "Haskell is working well for my team." ExpectLikert
, Question 21 "Haskell is critical to my company's success." ExpectLikert
, Question 22 "As a candidate, I can easily find Haskell jobs." ExpectLikert
, Question 23 "As a hiring manager, I can easily find qualified Haskell candidates." ExpectLikert
]
, Section 7 "Demographics"
[ Question 0 "Which country do you live in?" $ ExpectSingle
[ "Afghanistan"
, "Akrotiri"
, "Albania"
, "Algeria"
, "American Samoa"
, "Andorra"
, "Angola"
, "Anguilla"
, "Antarctica"
, "Antigua and Barbuda"
, "Argentina"
, "Armenia"
, "Aruba"
, "Ashmore and Cartier Islands"
, "Australia"
, "Austria"
, "Azerbaijan"
, "The Bahamas"
, "Bahrain"
, "Bangladesh"
, "Barbados"
, "Bassas da India"
, "Belarus"
, "Belgium"
, "Belize"
, "Benin"
, "Bermuda"
, "Bhutan"
, "Bolivia"
, "Bosnia and Herzegovina"
, "Botswana"
, "Bouvet Island"
, "Brazil"
, "British Indian Ocean Territory"
, "British Virgin Islands"
, "Brunei"
, "Bulgaria"
, "Burkina Faso"
, "Burma"
, "Burundi"
, "Cambodia"
, "Cameroon"
, "Canada"
, "Cape Verde"
, "Cayman Islands"
, "Central African Republic"
, "Chad"
, "Chile"
, "China"
, "Christmas Island"
, "Clipperton Island"
, "Cocos (Keeling) Islands"
, "Colombia"
, "Comoros"
, "Democratic Republic of the Congo"
, "Republic of the Congo"
, "Cook Islands"
, "Coral Sea Islands"
, "Costa Rica"
, "Cote d'Ivoire"
, "Croatia"
, "Cuba"
, "Cyprus"
, "Czech Republic"
, "Denmark"
, "Dhekelia"
, "Djibouti"
, "Dominica"
, "Dominican Republic"
, "Ecuador"
, "Egypt"
, "El Salvador"
, "Equatorial Guinea"
, "Eritrea"
, "Estonia"
, "Ethiopia"
, "Europa Island"
, "Falkland Islands (Islas Malvinas)"
, "Faroe Islands"
, "Fiji"
, "Finland"
, "France"
, "French Guiana"
, "French Polynesia"
, "French Southern and Antarctic Lands"
, "Gabon"
, "The Gambia"
, "Gaza Strip"
, "Georgia"
, "Germany"
, "Ghana"
, "Gibraltar"
, "Glorioso Islands"
, "Greece"
, "Greenland"
, "Grenada"
, "Guadeloupe"
, "Guam"
, "Guatemala"
, "Guernsey"
, "Guinea"
, "Guinea-Bissau"
, "Guyana"
, "Haiti"
, "Heard Island and McDonald Islands"
, "Holy See (Vatican City)"
, "Honduras"
, "Hong Kong"
, "Hungary"
, "Iceland"
, "India"
, "Indonesia"
, "Iran"
, "Iraq"
, "Ireland"
, "Isle of Man"
, "Israel"
, "Italy"
, "Jamaica"
, "Jan Mayen"
, "Japan"
, "Jersey"
, "Jordan"
, "Juan de Nova Island"
, "Kazakhstan"
, "Kenya"
, "Kiribati"
, "North Korea"
, "South Korea"
, "Kuwait"
, "Kyrgyzstan"
, "Laos"
, "Latvia"
, "Lebanon"
, "Lesotho"
, "Liberia"
, "Libya"
, "Liechtenstein"
, "Lithuania"
, "Luxembourg"
, "Macau"
, "Macedonia"
, "Madagascar"
, "Malawi"
, "Malaysia"
, "Maldives"
, "Mali"
, "Malta"
, "Marshall Islands"
, "Martinique"
, "Mauritania"
, "Mauritius"
, "Mayotte"
, "Mexico"
, "Federated States of Micronesia"
, "Moldova"
, "Monaco"
, "Mongolia"
, "Montserrat"
, "Morocco"
, "Mozambique"
, "Namibia"
, "Nauru"
, "Navassa Island"
, "Nepal"
, "Netherlands"
, "Netherlands Antilles"
, "New Caledonia"
, "New Zealand"
, "Nicaragua"
, "Niger"
, "Nigeria"
, "Niue"
, "Norfolk Island"
, "Northern Mariana Islands"
, "Norway"
, "Oman"
, "Pakistan"
, "Palau"
, "Palestine"
, "Panama"
, "Papua New Guinea"
, "Paracel Islands"
, "Paraguay"
, "Peru"
, "Philippines"
, "Pitcairn Islands"
, "Poland"
, "Portugal"
, "Puerto Rico"
, "Qatar"
, "Reunion"
, "Romania"
, "Russia"
, "Rwanda"
, "Saint Helena"
, "Saint Kitts and Nevis"
, "Saint Lucia"
, "Saint Pierre and Miquelon"
, "Saint Vincent and the Grenadines"
, "Samoa"
, "San Marino"
, "Sao Tome and Principe"
, "Saudi Arabia"
, "Senegal"
, "Serbia and Montenegro"
, "Seychelles"
, "Sierra Leone"
, "Singapore"
, "Slovakia"
, "Slovenia"
, "Solomon Islands"
, "Somalia"
, "South Africa"
, "South Georgia and the South Sandwich Islands"
, "Spain"
, "Spratly Islands"
, "Sri Lanka"
, "Sudan"
, "Suriname"
, "Svalbard"
, "Swaziland"
, "Sweden"
, "Switzerland"
, "Syria"
, "Taiwan"
, "Tajikistan"
, "Tanzania"
, "Thailand"
, "Timor-Leste"
, "Togo"
, "Tokelau"
, "Tonga"
, "Trinidad and Tobago"
, "Tromelin Island"
, "Tunisia"
, "Turkey"
, "Turkmenistan"
, "Turks and Caicos Islands"
, "Tuvalu"
, "Uganda"
, "Ukraine"
, "United Arab Emirates"
, "United Kingdom"
, "United States"
, "Uruguay"
, "Uzbekistan"
, "Vanuatu"
, "Venezuela"
, "Vietnam"
, "Virgin Islands"
, "Wake Island"
, "Wallis and Futuna"
, "West Bank"
, "Western Sahara"
, "Yemen"
, "Zambia"
, "Zimbabwe"
]
, Question 1 "How old are you?" $ ExpectSingle
[ "Under 18 years old"
, "18 to 24 years old"
, "25 to 34 years old"
, "35 to 44 years old"
, "45 to 54 years old"
, "55 to 64 years old"
, "Over 65 years old"
]
, Question 2 "What is your gender?" $ ExpectSingle
[ "Male"
, "Female"
, "Non-binary"
]
, Question 3 "Do you identify as transgender?" $ ExpectSingle
[ "Yes"
, "No"
]
, Question 4 "Are you a student?" $ ExpectSingle
[ "Yes, full time"
, "Yes, part time"
, "No"
]
, Question 5 "What is the highest level of education you have completed?" $ ExpectSingle
[ "Less than high school diploma"
, "High school diploma"
, "Some college"
, "Associate degree"
, "Bachelor's degree"
, "Master's degree"
, "Professional degree"
, "Doctoral degree"
]
, Question 6 "What is your employment status?" $ ExpectSingle
[ "Employed full time"
, "Employed part time"
, "Self employed"
, "Not employed, but looking for work"
, "Not employed, and not looking for work"
, "Retired"
]
, Question 7 "How large is the company you work for?" $ ExpectSingle
[ "Fewer than 10 employees"
, "10 to 99 employees"
, "100 to 999 employees"
, "More than 1,000 employees"
, "n/a"
]
, Question 8 "How many years have you been coding?" $ ExpectSingle
[ "0 to 4 years"
, "5 to 9 years"
, "10 to 14 years"
, "15 to 19 years"
, "20 to 24 years"
, "25 to 29 years"
, "30 or more years"
]
, Question 9 "How many years have you been coding professionally?" $ ExpectSingle
[ "0 to 4 years"
, "5 to 9 years"
, "10 to 14 years"
, "15 to 19 years"
, "20 to 24 years"
, "25 to 29 years"
, "30 or more years"
]
, Question 10 "Do you code as a hobby?" $ ExpectSingle
[ "Yes"
, "No"
]
, Question 11 "Have you contributed to any open source projects?" $ ExpectSingle
[ "Yes"
, "No"
]
]
, Section 8 "Meta"
[ Question 0 "Did you take any previous surveys?" $ ExpectMulti OtherReject
[ "2019"
, "2018"
, "2017"
]
, Question 1 "How did you hear about this survey?" $ ExpectSingle
[ "Discord"
, "Discourse"
, "GitHub"
, "Haskell Weekly"
, "Gitter"
, "In person"
, "IRC"
, "Lobsters"
, "Mailing lists"
, "Mastodon"
, "Matrix/Riot"
, "Reddit"
, "Slack"
, "Telegram"
, "Twitter"
, "Other"
]
]
, Section 9 "Free response"
[ Question 0 "If you wanted to convince someone to use Haskell, what would you say?" ExpectFree
, Question 1 "If you could change one thing about Haskell, what would it be?" ExpectFree
]
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment