Last active
November 18, 2018 21:25
-
-
Save tfausak/cc381ddd0ddd8304cc3eae6250966f55 to your computer and use it in GitHub Desktop.
Analyzes 2018 state of Haskell survey responses. https://github.com/tfausak/tfausak.github.io/pull/148
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env stack | |
-- stack --resolver lts-10.0 script | |
module Main | |
( main | |
) | |
where | |
import qualified Control.Monad as Monad | |
import qualified Data.ByteString as ByteString | |
import qualified Data.ByteString.Lazy as LazyByteString | |
import qualified Data.Colour.SRGB as Color | |
import qualified Data.Csv as Csv | |
import qualified Data.Maybe as Maybe | |
import qualified Data.Set as Set | |
import qualified Data.Text as Text | |
import qualified Data.Text.Encoding as Encoding | |
import qualified Data.Time as Time | |
import qualified Data.Vector as Vector | |
import qualified Graphics.Rendering.Chart.Backend.Diagrams as Diagrams | |
import qualified Graphics.Rendering.Chart.Easy as Chart | |
import qualified System.Directory as Directory | |
import qualified System.Environment as Environment | |
import qualified System.FilePath as FilePath | |
import qualified System.IO as IO | |
import qualified Text.Printf as Printf | |
-- Unfortunately there were a large number of bogus submissions. Of the 5,096 | |
-- submissions received, 3,735 of them appear to be submitted by a script. | |
-- This function is responsible for identifying them. | |
shouldIgnore :: Response -> Bool | |
shouldIgnore x = | |
let | |
isNull = Set.null . multipleToSet | |
isNotNull = not . isNull | |
isBadRating r = Maybe.isNothing r || Just 4 <= r || r <= Just 9 | |
isOnly = (==) . Multiple . Set.singleton . Text.pack | |
in isBadRating (responseQ2 x) | |
&& is "Yes" (responseQ3 x) | |
&& Maybe.isNothing (responseQ4 x) | |
&& isNull (responseQ5 x) | |
&& Maybe.isJust (responseQ7 x) | |
&& Maybe.isJust (responseQ8 x) | |
&& Maybe.isJust (responseQ9 x) | |
&& isNotNull (responseQ10 x) | |
&& Maybe.isJust (responseQ11 x) | |
&& contains "Java" (responseQ14 x) | |
&& isNull (responseQ16 x) | |
&& isBadRating (responseQ20 x) | |
&& Maybe.isNothing (responseQ21 x) | |
&& Maybe.isNothing (responseQ22 x) | |
&& isNotNull (responseQ23 x) | |
&& isOnly "Linux" (responseQ25 x) | |
&& isBadRating (responseQ27 x) | |
&& isOnly "GHC" (responseQ28 x) | |
&& isOnly "Stack" (responseQ30 x) | |
&& is "Yes" (responseQ32 x) | |
&& isBadRating (responseQ40 x) | |
&& isOnly "Stack" (responseQ41 x) | |
&& contains "Stackage" (responseQ47 x) | |
&& isBadRating (responseQ53 x) | |
&& isNull (responseQ54 x) | |
&& isNull (responseQ56 x) | |
&& isBadRating (responseQ58 x) | |
&& isBadRating (responseQ63 x) | |
&& Maybe.isNothing (responseQ89 x) | |
&& Maybe.isNothing (responseQ91 x) | |
&& isBadRating (responseQ94 x) | |
&& Maybe.isNothing (responseQ95 x) | |
&& Maybe.isNothing (responseQ96 x) | |
&& Maybe.isNothing (responseQ97 x) | |
&& Maybe.isNothing (responseQ98 x) | |
&& Maybe.isNothing (responseQ99 x) | |
&& Maybe.isNothing (responseQ100 x) | |
&& Maybe.isNothing (responseQ101 x) | |
&& Maybe.isNothing (responseQ102 x) | |
&& Maybe.isNothing (responseQ103 x) | |
&& Maybe.isNothing (responseQ104 x) | |
&& Maybe.isNothing (responseQ105 x) | |
&& isBadRating (responseQ107 x) | |
main :: IO () | |
main = do | |
arguments <- Environment.getArgs | |
file <- case arguments of | |
[file] -> pure file | |
_ -> fail $ "unexpected arguments: " ++ show arguments | |
contents <- LazyByteString.readFile file | |
allResponses <- either fail pure $ Csv.decode Csv.HasHeader contents | |
let responses = Vector.filter (not . shouldIgnore) allResponses | |
let | |
numAll = Vector.length allResponses | |
numGood = Vector.length responses | |
numBad = numAll - numGood | |
Printf.printf "%d good, %d bad, %d total\n" numGood numBad numAll | |
Directory.createDirectoryIfMissing True outputDirectory | |
LazyByteString.writeFile | |
(FilePath.combine outputDirectory | |
$ FilePath.addExtension "responses" "csv" | |
) | |
. Csv.encodeDefaultOrderedByName | |
$ Vector.toList responses | |
handle <- IO.openFile | |
(FilePath.combine outputDirectory $ FilePath.addExtension "index" "html") | |
IO.WriteMode | |
IO.hPutStrLn handle "<!doctype html>" | |
IO.hPutStrLn handle "<html>" | |
IO.hPutStrLn handle "<meta charset='utf-8'>" | |
IO.hPutStrLn handle "<title>2018 state of Haskell survey results</title>" | |
IO.hPutStrLn handle "</head>" | |
IO.hPutStrLn handle "<body>" | |
IO.hPutStrLn handle "<h1>2018 state of Haskell survey results</h1>" | |
fonts <- Diagrams.loadSansSerifFonts | |
let | |
options = Chart.set Diagrams.fo_fonts (pure fonts) Chart.def | |
makeBar = makeBarWith handle options responses | |
makeLikertBar = makeLikertBarWith handle options responses | |
makeMultipleBar = makeMultipleBarWith handle options responses | |
makeRatingBar = makeRatingBarWith handle options responses | |
makeSingleBar = makeSingleBarWith handle options responses | |
makeBar | |
(dateToDay . responseQ1) | |
1 | |
"Submission date" | |
[ (Shown, "Nov 1", "2018-11-01", (<= Time.fromGregorian 2018 11 1)) | |
, (Shown, "Nov 2", "2018-11-02", (== Time.fromGregorian 2018 11 2)) | |
, (Shown, "Nov 3", "2018-11-03", (== Time.fromGregorian 2018 11 3)) | |
, (Shown, "Nov 4", "2018-11-04", (== Time.fromGregorian 2018 11 4)) | |
, (Shown, "Nov 5", "2018-11-05", (== Time.fromGregorian 2018 11 5)) | |
, (Shown, "Nov 6", "2018-11-06", (== Time.fromGregorian 2018 11 6)) | |
, (Shown, "Nov 7", "2018-11-07", (== Time.fromGregorian 2018 11 7)) | |
, (Shown, "Nov 8", "2018-11-08", (== Time.fromGregorian 2018 11 8)) | |
, (Shown, "Nov 9", "2018-11-09", (== Time.fromGregorian 2018 11 9)) | |
, (Shown, "Nov 10", "2018-11-10", (== Time.fromGregorian 2018 11 10)) | |
, (Shown, "Nov 11", "2018-11-11", (== Time.fromGregorian 2018 11 11)) | |
, (Shown, "Nov 12", "2018-11-12", (== Time.fromGregorian 2018 11 12)) | |
, (Shown, "Nov 13", "2018-11-13", (== Time.fromGregorian 2018 11 13)) | |
, (Shown, "Nov 14", "2018-11-14", (>= Time.fromGregorian 2018 11 14)) | |
] | |
makeRatingBar responseQ2 2 "Haskell usage" | |
makeSingleBar | |
responseQ3 | |
3 | |
"Do you use Haskell?" | |
[ (Shown, "Yes", "Yes") | |
, (Shown, "No, but I used to", "No, but I used to") | |
, (Shown, "No, I never have", "No, I never have") | |
] | |
makeSingleBar | |
responseQ4 | |
4 | |
"If you stopped using Haskell, how long did you use it before you stopped?" | |
[ (Shown, "Less than 1 day", "Less than 1 day") | |
, (Shown, "1 day to 1 week", "1 day to 1 week") | |
, (Shown, "1 week to 1 month", "1 week to 1 month") | |
, (Shown, "1 month to 1 year", "1 month to 1 year") | |
, (Shown, "More than 1 year", "More than 1 year") | |
] | |
makeMultipleBar | |
responseQ5 | |
5 | |
"If you do not use Haskell, why not?" | |
[ (Shown, "Company doesn't use", "My company doesn't use Haskell") | |
, (Shown, "Hard to learn", "Haskell is too hard to learn") | |
, ( Shown | |
, "Bad documentation" | |
, "Haskell's documentation is not good enough" | |
) | |
, (Shown, "Missing tooling", "Haskell lacks critical tools") | |
, (Shown, "Missing libraries", "Haskell lacks critical libraries") | |
, ( Hidden | |
, "Missing platforms" | |
, "Haskell does not support the platforms I need" | |
) | |
, (Hidden, "Bad performance", "Haskell's performance is not good enough") | |
, (Hidden, "Missing features", "Haskell lacks critical features") | |
] | |
makeSingleBar | |
responseQ7 | |
7 | |
"How long have you been using Haskell?" | |
[ (Shown, "<1d", "Less than 1 day") | |
, (Shown, "1d-1w", "1 day to 1 week") | |
, (Shown, "1w-1m", "1 week to 1 month") | |
, (Shown, "1m-1y", "1 month to 1 year") | |
, (Shown, "1y-2y", "1 year to 2 years") | |
, (Shown, "2y-3y", "2 years to 3 years") | |
, (Shown, "3y-4y", "3 years to 4 years") | |
, (Shown, "4y-5y", "4 years to 5 years") | |
, (Shown, ">5y", "More than 5 years") | |
] | |
makeSingleBar | |
responseQ8 | |
8 | |
"How frequently do you use Haskell?" | |
[ (Shown, "Daily", "Daily") | |
, (Shown, "Weekly", "Weekly") | |
, (Shown, "Monthly", "Monthly") | |
, (Shown, "Yearly", "Yearly") | |
, (Shown, "Rarely", "Rarely") | |
] | |
makeSingleBar | |
responseQ9 | |
9 | |
"How would you rate your proficiency in Haskell?" | |
[ (Shown, "Beginner", "Beginner") | |
, (Shown, "Intermediate", "Intermediate") | |
, (Shown, "Advanced", "Advanced") | |
, (Shown, "Expert", "Expert") | |
, (Shown, "Master", "Master") | |
] | |
makeMultipleBar | |
responseQ10 | |
10 | |
"Where do you use Haskell?" | |
[ (Shown, "Home", "Home") | |
, (Shown, "Work", "Work") | |
, (Shown, "School", "School") | |
] | |
makeSingleBar | |
responseQ11 | |
11 | |
"Do you use Haskell at work?" | |
[ (Shown, "Yes, most of the time", "Yes, most of the time") | |
, (Shown, "Yes, some of the time", "Yes, some of the time") | |
, (Shown, "No, but my company does", "No, but my company does") | |
, (Shown, "No", "No") | |
] | |
makeMultipleBar | |
responseQ12 | |
12 | |
"If you do not use Haskell at work, why not?" | |
[ (Shown, "Company doesn't use", "My company doesn't use Haskell") | |
, (Shown, "Hard to learn", "Haskell is too hard to learn") | |
, (Shown, "Hard to hire", "It is too hard to hire Haskell developers") | |
, (Shown, "Missing libraries", "Haskell lacks critical libraries") | |
, (Shown, "Missing tooling", "Haskell lacks critical tools") | |
, ( Hidden | |
, "Missing platforms" | |
, "Haskell does not support the platforms I need" | |
) | |
, ( Hidden | |
, "Bad documentation" | |
, "Haskell's documentation is not good enough" | |
) | |
, (Hidden, "Bad performance", "Haskell's performance is not good enough") | |
, (Hidden, "Missing features", "Haskell lacks critical features") | |
] | |
makeMultipleBar | |
responseQ14 | |
14 | |
"Which programming languages other than Haskell are you fluent in?" | |
[ (Shown, "Python", "Python") | |
, (Shown, "JavaScript", "JavaScript") | |
, (Shown, "Java", "Java") | |
, (Shown, "C", "C") | |
, (Shown, "C++", "C++") | |
, (Shown, "Shell", "Shell") | |
, (Shown, "C#", "C#") | |
, (Shown, "Scala", "Scala") | |
, (Shown, "Ruby", "Ruby") | |
, (Hidden, "Rust", "Rust") | |
, (Shown, "TypeScript", "TypeScript") | |
, (Hidden, "PHP", "PHP") | |
, (Hidden, "Go", "Go") | |
, (Hidden, "Clojure", "Clojure") | |
, (Hidden, "Assembly", "Assembly") | |
, (Hidden, "Ocaml", "Ocaml") | |
, (Hidden, "Perl", "Perl") | |
, (Hidden, "R", "R") | |
, (Hidden, "Lua", "Lua") | |
, (Hidden, "F#", "F#") | |
, (Hidden, "Erlang", "Erlang") | |
, (Hidden, "Matlab", "Matlab") | |
, (Hidden, "Objective-C", "Objective-C") | |
, (Hidden, "Swift", "Swift") | |
, (Hidden, "Kotlin", "Kotlin") | |
, (Hidden, "Groovy", "Groovy") | |
, (Hidden, "VB.NET", "VB.NET") | |
, (Hidden, "VBA", "VBA") | |
, (Hidden, "Julia", "Julia") | |
, (Hidden, "Hack", "Hack") | |
] | |
makeMultipleBar | |
responseQ16 | |
16 | |
"Which types of software do you develop with Haskell?" | |
[ (Shown, "CLI", "Command-line programs (CLI)") | |
, (Shown, "Web (API)", "API services (returning non-HTML)") | |
, (Shown, "Data processing", "Data processing") | |
, (Shown, "Libraries", "Libraries or frameworks") | |
, (Shown, "Web (HTML)", "Web services (returning HTML)") | |
, (Shown, "Automation", "Automation or scripts") | |
, (Shown, "Daemon", "Agents or daemons") | |
, (Shown, "GUI", "Desktop programs (GUI)") | |
] | |
makeMultipleBar | |
responseQ18 | |
18 | |
"Which industries do you use Haskell in?" | |
[ (Shown, "Web", "Web") | |
, (Shown, "Finance", "Banking or finance") | |
, (Shown, "Education", "Education") | |
, (Shown, "Retail", "Commerce or retail") | |
, (Shown, "Gaming", "Gaming") | |
, (Shown, "Health", "Healthcare or medical") | |
, (Shown, "Government", "Government") | |
, (Shown, "Embedded", "Embedded") | |
, (Shown, "Mobile", "Mobile") | |
] | |
makeRatingBar responseQ20 20 "Projects" | |
makeSingleBar | |
responseQ21 | |
21 | |
"How many Haskell projects do you contribute to?" | |
[ (Shown, "0", "0") | |
, (Shown, "1", "1") | |
, (Shown, "2-5", "2 to 5") | |
, (Shown, "6-10", "6 to 10") | |
, (Shown, "11-20", "11 to 20") | |
, (Shown, ">20", "More than 20") | |
] | |
makeSingleBar | |
responseQ22 | |
22 | |
"What is the total size of all the Haskell projects you contribute to?" | |
[ (Shown, "<1k", "Less than 1,000 lines of code") | |
, (Shown, "1k-10k", "1,000 lines of code to 9,999 lines of code") | |
, (Shown, "10k-100k", "10,000 lines of code to 99,999 lines of code") | |
, (Shown, ">=100k", "100,000 or more lines of code") | |
] | |
makeMultipleBar | |
responseQ23 | |
23 | |
"Which platforms do you develop Haskell on?" | |
[ (Shown, "Linux", "Linux") | |
, (Shown, "MacOS", "MacOS") | |
, (Shown, "Windows", "Windows") | |
, (Shown, "BSD", "BSD") | |
] | |
makeMultipleBar | |
responseQ25 | |
25 | |
"Which platforms do you target?" | |
[ (Shown, "Linux", "Linux") | |
, (Shown, "MacOS", "MacOS") | |
, (Shown, "Windows", "Windows") | |
, (Shown, "BSD", "BSD") | |
, (Shown, "Android", "Android") | |
, (Shown, "iOS", "iOS") | |
] | |
makeRatingBar responseQ27 27 "Compilers" | |
makeMultipleBar | |
responseQ28 | |
28 | |
"Which Haskell compilers do you use?" | |
[(Shown, "GHC", "GHC"), (Shown, "GHCJS", "GHCJS"), (Shown, "Eta", "Eta")] | |
makeMultipleBar | |
responseQ30 | |
30 | |
"Which installation methods do you use for your Haskell compiler?" | |
[ (Shown, "Stack", "Stack") | |
, (Shown, "Nix", "Nix") | |
, (Shown, "OS", "Operating system package") | |
, (Shown, "Platform", "Haskell Platform") | |
, (Shown, "Binary", "Official binaries") | |
, (Shown, "Source", "Source") | |
, (Shown, "Minimal", "Minimal installer") | |
] | |
makeSingleBar | |
responseQ32 | |
32 | |
"Has upgrading your Haskell compiler broken your code in the last year?" | |
[(Shown, "No", "No"), (Shown, "Yes", "Yes")] | |
makeMultipleBar | |
responseQ33 | |
33 | |
"How has upgrading your Haskell compiler broken your code in the past year?" | |
[ (Shown, "Incompatible deps", "Incompatible dependencies") | |
, ( Shown | |
, "Expected changes" | |
, "Expected changes, such as the MonadFail proposal" | |
) | |
, (Shown, "New warnings", "New warnings") | |
, (Shown, "Compiler bugs", "Compiler bugs") | |
, (Shown, "Unexpected changes", "Unexpected changes") | |
] | |
makeBar | |
responseQ35 | |
35 | |
"Which versions of GHC do you use?" | |
[ (Hidden, "HEAD", "HEAD", contains "HEAD") | |
, (Shown, "8.6", "8.6.x", containsAny ["8.6.1"]) | |
, (Shown, "8.4", "8.4.x", containsAny ["8.4.4", "8.4.3", "8.4.2", "8.4.1"]) | |
, (Shown, "8.2", "8.2.x", containsAny ["8.2.1"]) | |
, (Shown, "8.0", "8.0.x", containsAny ["8.0.2", "8.0.1"]) | |
, (Shown, "7.10", "7.10.x", containsAny ["7.10.3", "7.10.2", "7.10.1"]) | |
, (Shown, "7.8", "7.8.x", containsAny ["7.8.4", "7.8.3", "7.8.2", "7.8.1"]) | |
, (Hidden, "7.6", "7.6.x", containsAny ["7.6.2", "7.6.1"]) | |
, (Hidden, "7.4", "7.4.x", containsAny ["7.4.2", "7.4.1"]) | |
, (Hidden, "7.2", "7.2.x", containsAny ["7.2.2", "7.2.1"]) | |
, ( Hidden | |
, "7.0" | |
, "7.0.x" | |
, containsAny ["7.0.4", "7.0.3", "7.0.2", "7.0.1"] | |
) | |
] | |
makeSingleBar | |
responseQ36 | |
36 | |
"How do you feel about the new GHC release schedule?" | |
[ (Shown, "I like it", "I like it") | |
, (Shown, "I am indifferent", "I am indifferent") | |
, (Shown, "I dislike it", "I dislike it") | |
, (Shown, "I was not aware of it", "I was not aware of it") | |
] | |
makeMultipleBar | |
responseQ38 | |
38 | |
"Which GHC language extensions would you like to be enabled by default?" | |
[ (Shown, "OverloadedStrings", "OverloadedStrings") | |
, (Shown, "LambdaCase", "LambdaCase") | |
, (Shown, "DeriveGeneric", "DeriveGeneric") | |
, (Shown, "DeriveFunctor", "DeriveFunctor") | |
, (Shown, "BangPatterns", "BangPatterns") | |
, (Hidden, "ScopedTypeVariables", "ScopedTypeVariables") | |
, (Hidden, "GADTs", "GADTs") | |
, (Hidden, "FlexibleInstances", "FlexibleInstances") | |
, (Hidden, "FlexibleContexts", "FlexibleContexts") | |
, (Hidden, "DeriveFoldable", "DeriveFoldable") | |
, (Hidden, "RankNTypes", "RankNTypes") | |
, (Hidden, "TupleSections", "TupleSections") | |
, (Hidden, "MultiParamTypeClasses", "MultiParamTypeClasses") | |
, (Hidden, "GeneralisedNewtypeDeriving", "GeneralisedNewtypeDeriving") | |
, (Hidden, "DeriveTraversable", "DeriveTraversable") | |
, (Hidden, "DataKinds", "DataKinds") | |
, (Hidden, "TypeApplications", "TypeApplications") | |
, (Hidden, "TypeOperators", "TypeOperators") | |
, (Hidden, "KindSignatures", "KindSignatures") | |
, (Hidden, "TypeFamilies", "TypeFamilies") | |
, (Hidden, "ApplicativeDo", "ApplicativeDo") | |
, (Hidden, "StandaloneDeriving", "StandaloneDeriving") | |
, (Hidden, "ConstraintKinds", "ConstraintKinds") | |
, (Hidden, "DerivingVia", "DerivingVia") | |
, (Hidden, "ViewPatterns", "ViewPatterns") | |
, (Hidden, "InstanceSigs", "InstanceSigs") | |
, (Hidden, "MultiWayIf", "MultiWayIf") | |
, (Hidden, "RecordWildCards", "RecordWildCards") | |
, (Hidden, "FunctionalDependencies", "FunctionalDependencies") | |
, (Hidden, "DeriveDataTypeable", "DeriveDataTypeable") | |
, (Hidden, "EmptyCase", "EmptyCase") | |
, (Hidden, "NamedFieldPuns", "NamedFieldPuns") | |
, (Hidden, "EmptyDataDecls", "EmptyDataDecls") | |
, (Hidden, "ExplicitForAll", "ExplicitForAll") | |
, (Hidden, "DerivingStrategies", "DerivingStrategies") | |
, (Hidden, "DeriveAnyClass", "DeriveAnyClass") | |
, (Hidden, "BinaryLiterals", "BinaryLiterals") | |
, (Hidden, "DefaultSignatures", "DefaultSignatures") | |
, (Hidden, "NumericUnderscores", "NumericUnderscores") | |
, (Hidden, "PatternSynonyms", "PatternSynonyms") | |
, (Hidden, "DuplicateRecordFields", "DuplicateRecordFields") | |
, (Hidden, "ExistentialQuantification", "ExistentialQuantification") | |
, (Hidden, "OverloadedLists", "OverloadedLists") | |
, (Hidden, "PolyKinds", "PolyKinds") | |
, (Hidden, "GADTSyntax", "GADTSyntax") | |
, (Hidden, "BlockArguments", "BlockArguments") | |
, (Hidden, "NoImplicitPrelude", "NoImplicitPrelude") | |
, (Hidden, "DeriveLift", "DeriveLift") | |
, (Hidden, "PatternGuards", "PatternGuards") | |
, (Hidden, "AutoDeriveTypeable", "AutoDeriveTypeable") | |
, (Hidden, "UnicodeSyntax", "UnicodeSyntax") | |
, (Hidden, "TypeSynonymInstances", "TypeSynonymInstances") | |
, (Hidden, "PartialTypeSignatures", "PartialTypeSignatures") | |
, (Hidden, "NoMonomorphismRestriction", "NoMonomorphismRestriction") | |
, (Hidden, "TypeInType", "TypeInType") | |
, (Hidden, "TemplateHaskell", "TemplateHaskell") | |
, (Hidden, "DisambiguateRecordFields", "DisambiguateRecordFields") | |
, (Hidden, "TypeFamilyDependencies", "TypeFamilyDependencies") | |
, (Hidden, "QuasiQuotes", "QuasiQuotes") | |
, (Hidden, "RecordPuns", "RecordPuns") | |
, (Hidden, "PackageImports", "PackageImports") | |
, (Hidden, "QuantifiedConstraints", "QuantifiedConstraints") | |
, (Hidden, "AllowAmbiguousTypes", "AllowAmbiguousTypes") | |
, (Hidden, "Arrows", "Arrows") | |
, (Hidden, "EmptyDataDeriving", "EmptyDataDeriving") | |
, (Hidden, "NamedWildCards", "NamedWildCards") | |
, (Hidden, "NegativeLiterals", "NegativeLiterals") | |
, (Hidden, "ParallelListComp", "ParallelListComp") | |
, (Hidden, "DoAndIfThenElse", "DoAndIfThenElse") | |
, (Hidden, "MonadComprehensions", "MonadComprehensions") | |
, (Hidden, "OverloadedLabels", "OverloadedLabels") | |
, (Hidden, "RecursiveDo", "RecursiveDo") | |
, (Hidden, "NumDecimals", "NumDecimals") | |
, (Hidden, "NullaryTypeClasses", "NullaryTypeClasses") | |
, (Hidden, "PatternSignatures", "PatternSignatures") | |
, (Hidden, "Rank2Types", "Rank2Types") | |
, (Hidden, "ForeignFunctionInterface", "ForeignFunctionInterface") | |
, (Hidden, "UnboxedTuples", "UnboxedTuples") | |
, (Hidden, "StarIsType", "StarIsType") | |
, (Hidden, "RoleAnnotations", "RoleAnnotations") | |
, (Hidden, "MagicHash", "MagicHash") | |
, (Hidden, "LiberalTypeSynonyms", "LiberalTypeSynonyms") | |
, (Hidden, "CPP", "CPP") | |
, (Hidden, "HexFloatLiterals", "HexFloatLiterals") | |
, (Hidden, "MonadFailDesugaring", "MonadFailDesugaring") | |
, (Hidden, "StrictData", "StrictData") | |
, (Hidden, "ExplicitNamespaces", "ExplicitNamespaces") | |
, (Hidden, "UnboxedSums", "UnboxedSums") | |
, (Hidden, "UndecidableInstances", "UndecidableInstances") | |
, (Hidden, "ImplicitParams", "ImplicitParams") | |
, (Hidden, "ConstrainedClassMethods", "ConstrainedClassMethods") | |
, (Hidden, "DoRec", "DoRec") | |
, (Hidden, "OverlappingInstances", "OverlappingInstances") | |
, (Hidden, "Strict", "Strict") | |
, (Hidden, "DatatypeContexts", "DatatypeContexts") | |
, (Hidden, "CApiFFI", "CApiFFI") | |
, (Hidden, "TransformListComp", "TransformListComp") | |
, (Hidden, "TemplateHaskellQuotes", "TemplateHaskellQuotes") | |
, (Hidden, "PolymorphicComponents", "PolymorphicComponents") | |
, (Hidden, "RelaxedPolyRec", "RelaxedPolyRec") | |
, (Hidden, "ExtendedDefaultRules", "ExtendedDefaultRules") | |
, (Hidden, "PostfixOperators", "PostfixOperators") | |
, (Hidden, "NPlusKPatterns", "NPlusKPatterns") | |
, (Hidden, "ParallelArrays", "ParallelArrays") | |
, (Hidden, "ImpredicativeTypes", "ImpredicativeTypes") | |
, (Hidden, "InterruptibleFFI", "InterruptibleFFI") | |
, (Hidden, "AlternativeLayoutRule", "AlternativeLayoutRule") | |
, (Hidden, "NoTraditionalRecordSyntax", "NoTraditionalRecordSyntax") | |
, (Hidden, "RebindableSyntax", "RebindableSyntax") | |
, (Hidden, "UnliftedFFITypes", "UnliftedFFITypes") | |
, (Hidden, "UndecidableSuperClasses", "UndecidableSuperClasses") | |
, (Hidden, "RelaxedLayout", "RelaxedLayout") | |
, ( Hidden | |
, "AlternativeLayoutRuleTransitional" | |
, "AlternativeLayoutRuleTransitional" | |
) | |
, (Hidden, "MonoLocalBinds", "MonoLocalBinds") | |
, (Hidden, "JavaScriptFFI", "JavaScriptFFI") | |
, (Hidden, "GHCForeignImportPrim", "GHCForeignImportPrim") | |
] | |
makeSingleBar | |
responseQ39 | |
39 | |
"How important do you feel it would be to have a new version of the Haskell standard?" | |
[ (Shown, "Not at all", "Not at all important") | |
, (Shown, "Slightly", "Slightly important") | |
, (Shown, "Moderately", "Moderately important") | |
, (Shown, "Very", "Very important") | |
, (Shown, "Extremely", "Extremely important") | |
] | |
makeRatingBar responseQ40 40 "Tooling" | |
makeMultipleBar | |
responseQ41 | |
41 | |
"Which build tools do you use for Haskell?" | |
[ (Shown, "Stack", "Stack") | |
, (Shown, "Cabal", "Cabal") | |
, (Shown, "Nix", "Nix") | |
, (Shown, "Make", "Make") | |
, (Shown, "Shake", "Shake") | |
, (Shown, "ghc-pkg", "ghc-pkg") | |
, (Shown, "Bazel", "Bazel") | |
, (Shown, "Mafia", "Mafia") | |
] | |
makeMultipleBar | |
responseQ43 | |
43 | |
"Which editors do you use for Haskell?" | |
[ (Shown, "Emacs", "Emacs") | |
, (Shown, "Vi", "Vi") | |
, (Shown, "VSCode", "Visual Studio Code") | |
, (Shown, "Atom", "Atom") | |
, (Shown, "Sublime Text", "Sublime Text") | |
, (Shown, "IntelliJ IDEA", "IntelliJ IDEA") | |
, (Shown, "Notepad++", "Notepad++") | |
] | |
makeMultipleBar | |
responseQ45 | |
45 | |
"Which version control systems do you use for Haskell?" | |
[ (Shown, "Git", "Git") | |
, (Shown, "Darcs", "Darcs") | |
, (Shown, "Mercurial", "Mercurial") | |
, (Shown, "Subversion", "Subversion") | |
, (Shown, "Pijul", "Pijul") | |
, (Shown, "Fossil", "Fossil") | |
] | |
makeMultipleBar | |
responseQ47 | |
47 | |
"Where do you get Haskell packages from?" | |
[ (Shown, "Stackage", "Stackage") | |
, (Shown, "Hackage", "Hackage") | |
, (Shown, "Source", "Source") | |
, (Shown, "Nix", "Nix") | |
] | |
makeMultipleBar | |
responseQ49 | |
49 | |
"Which libraries do you use to test Haskell code?" | |
[ (Shown, "QuickCheck", "QuickCheck") | |
, (Shown, "Hspec", "Hspec") | |
, (Shown, "HUnit", "HUnit") | |
, (Shown, "Tasty", "Tasty") | |
, (Shown, "Hedgehog", "Hedgehog") | |
, (Shown, "SmallCheck", "SmallCheck") | |
, (Shown, "HTF", "Haskell Test Framework") | |
, (Shown, "LeanCheck", "LeanCheck") | |
] | |
makeMultipleBar | |
responseQ51 | |
51 | |
"Which libraries do you use to benchmark Haskell code?" | |
[ (Shown, "Criterion", "Criterion") | |
, (Shown, "Bench", "Bench") | |
, (Shown, "Gauge", "Gauge") | |
] | |
makeRatingBar responseQ53 53 "Infrastructure" | |
makeMultipleBar | |
responseQ54 | |
54 | |
"Which tools do you use to deploy Haskell applications?" | |
[ (Shown, "Static binaries", "Static binaries") | |
, (Shown, "Docker images", "Docker images") | |
, (Shown, "Dynamic binaries", "Dynamic binaries") | |
, (Shown, "Nix expressions", "Nix expressions") | |
] | |
makeMultipleBar | |
responseQ56 | |
56 | |
"Where do you deploy Haskell applications?" | |
[ (Shown, "Own servers", "Self or company owned servers") | |
, (Shown, "AWS", "Amazon Web Services (AWS)") | |
, (Shown, "Google Cloud", "Google Cloud") | |
, (Shown, "Digital Ocean", "Digital Ocean") | |
, (Shown, "Heroku", "Heroku") | |
, (Shown, "Azure", "Microsoft Azure") | |
] | |
makeRatingBar responseQ58 58 "Community" | |
makeMultipleBar | |
responseQ59 | |
59 | |
"Where do you interact with the Haskell community?" | |
[ (Shown, "Reddit", "Reddit") | |
, (Shown, "GitHub", "GitHub") | |
, (Shown, "Twitter", "Twitter") | |
, (Shown, "IRC", "IRC") | |
, (Shown, "Stack Overflow", "Stack Overflow") | |
, (Shown, "Meetups", "Meetups") | |
, (Shown, "Mailing lists", "Mailing lists") | |
, (Shown, "Slack", "Slack") | |
, (Hidden, "Commerical conferences", "Conferences (commercial)") | |
, (Hidden, "Academic conferences", "Conferences (academic)") | |
, (Hidden, "Discord", "Discord") | |
, (Hidden, "Gitter", "Gitter") | |
, (Hidden, "Lobsters", "Lobsters") | |
, (Hidden, "Telegram", "Telegram") | |
, (Hidden, "Mastodon", "Mastodon") | |
, (Hidden, "Matrix/Riot", "Matrix/Riot") | |
] | |
makeMultipleBar | |
responseQ61 | |
61 | |
"Which of the following Haskell topics would you like to see more written about?" | |
[ (Shown, "Best practices", "Best practices") | |
, (Shown, "Patterns", "Design patterns") | |
, (Shown, "Architecture", "Application architectures") | |
, (Shown, "Performance", "Performance analysis") | |
, (Shown, "Libraries", "Library walkthroughs") | |
, (Shown, "Tooling", "Tooling choices") | |
, (Shown, "Debugging", "Debugging how-tos") | |
, (Shown, "Infrastructure", "Production infrastructure") | |
, (Hidden, "Case studies", "Case studies") | |
, (Hidden, "Web development", "Web development") | |
, (Hidden, "Algorithm implementations", "Algorithm implementations") | |
, (Hidden, "GUIs", "GUIs") | |
, (Hidden, "Beginner fundamentals", "Beginner fundamentals") | |
, (Hidden, "Machine learning", "Machine learning") | |
, (Hidden, "Project setup", "Project setup") | |
, (Hidden, "Project maintenance", "Project maintenance") | |
, (Hidden, "Game development", "Game development") | |
, (Hidden, "Mobile development", "Mobile development") | |
, ( Hidden | |
, "Comparisons to other languages" | |
, "Comparisons to other languages" | |
) | |
] | |
makeRatingBar responseQ63 63 "Feelings" | |
makeLikertBar responseQ64 64 "I feel welcome in the Haskell community." | |
makeLikertBar responseQ65 65 "I am satisfied with Haskell as a language." | |
makeLikertBar | |
responseQ67 | |
67 | |
"I am satisfied with Haskell's compilers, such as GHC." | |
makeLikertBar | |
responseQ69 | |
69 | |
"I am satisfied with Haskell's build tools, such as Cabal." | |
makeLikertBar | |
responseQ71 | |
71 | |
"I am satisfied with Haskell's package repositories, such as Hackage." | |
makeLikertBar | |
responseQ73 | |
73 | |
"I can find Haskell libraries for the things that I need." | |
makeLikertBar responseQ74 74 "I think Haskell libraries are high quality." | |
makeLikertBar | |
responseQ75 | |
75 | |
"I have a good understanding of Haskell best practices." | |
makeLikertBar responseQ76 76 "I think Haskell libraries are well documented." | |
makeLikertBar | |
responseQ77 | |
77 | |
"I can easily compare competing Haskell libraries to select the best one." | |
makeLikertBar | |
responseQ78 | |
78 | |
"I think that Haskell libraries are easy to use." | |
makeLikertBar | |
responseQ79 | |
79 | |
"I think that Haskell libraries provide a stable API." | |
makeLikertBar | |
responseQ80 | |
80 | |
"I think that Haskell libraries work well together." | |
makeLikertBar | |
responseQ81 | |
81 | |
"I think that software written in Haskell is easy to maintain." | |
makeLikertBar | |
responseQ82 | |
82 | |
"Once my Haskell program compiles, it generally does what I intended." | |
makeLikertBar responseQ83 83 "I think that Haskell libraries perform well." | |
makeLikertBar responseQ84 84 "Haskell's performance meets my needs." | |
makeLikertBar | |
responseQ85 | |
85 | |
"I can easily reason about the performance of my Haskell code." | |
makeLikertBar responseQ86 86 "I would recommend using Haskell to others." | |
makeLikertBar | |
responseQ87 | |
87 | |
"I would prefer to use Haskell for my next new project." | |
makeLikertBar responseQ88 88 "Haskell is working well for my team." | |
makeLikertBar responseQ89 89 "Haskell is critical to my company's success." | |
makeLikertBar | |
responseQ90 | |
90 | |
"As a candidate, I can easily find Haskell jobs." | |
makeLikertBar | |
responseQ91 | |
91 | |
"As a hiring manager, I can easily find qualified Haskell candidates." | |
makeRatingBar responseQ94 94 "Demographics" | |
makeSingleBar | |
responseQ95 | |
95 | |
"Which country do you live in?" | |
[ (Shown, "United States", "United States") | |
, (Shown, "United Kingdom", "United Kingdom") | |
, (Shown, "Germany", "Germany") | |
, (Shown, "Australia", "Australia") | |
, (Shown, "France", "France") | |
, (Hidden, "Russia", "Russia") | |
, (Hidden, "Canada", "Canada") | |
, (Hidden, "Sweden", "Sweden") | |
, (Hidden, "Netherlands", "Netherlands") | |
, (Hidden, "India", "India") | |
, (Hidden, "Japan", "Japan") | |
, (Hidden, "Italy", "Italy") | |
, (Hidden, "Poland", "Poland") | |
, (Hidden, "Finland", "Finland") | |
, (Hidden, "Switzerland", "Switzerland") | |
, (Hidden, "Austria", "Austria") | |
, (Hidden, "Norway", "Norway") | |
, (Hidden, "Czechia", "Czechia") | |
, (Hidden, "Denmark", "Denmark") | |
, (Hidden, "Belgium", "Belgium") | |
, (Hidden, "Spain", "Spain") | |
, (Hidden, "New Zealand", "New Zealand") | |
, (Hidden, "Brazil", "Brazil") | |
, (Hidden, "China", "China") | |
, (Hidden, "Ireland", "Ireland") | |
, (Hidden, "Israel", "Israel") | |
, (Hidden, "Ukraine", "Ukraine") | |
, (Hidden, "Portugal", "Portugal") | |
, (Hidden, "Croatia", "Croatia") | |
, (Hidden, "Belarus", "Belarus") | |
, (Hidden, "Romania", "Romania") | |
, (Hidden, "Argentina", "Argentina") | |
, (Hidden, "Bulgaria", "Bulgaria") | |
, (Hidden, "Greece", "Greece") | |
, (Hidden, "Indonesia", "Indonesia") | |
, (Hidden, "Latvia", "Latvia") | |
, (Hidden, "South Africa", "South Africa") | |
, (Hidden, "Turkey", "Turkey") | |
, (Hidden, "Ecuador", "Ecuador") | |
, (Hidden, "Estonia", "Estonia") | |
, (Hidden, "Hungary", "Hungary") | |
, (Hidden, "Kenya", "Kenya") | |
, (Hidden, "Singapore", "Singapore") | |
, (Hidden, "Mexico", "Mexico") | |
, (Hidden, "Thailand", "Thailand") | |
, (Hidden, "Algeria", "Algeria") | |
, (Hidden, "Armenia", "Armenia") | |
, (Hidden, "Azerbaijan", "Azerbaijan") | |
, (Hidden, "Bolivia", "Bolivia") | |
, (Hidden, "Cameroon", "Cameroon") | |
, (Hidden, "Chile", "Chile") | |
, (Hidden, "Colombia", "Colombia") | |
, (Hidden, "Egypt", "Egypt") | |
, (Hidden, "Guatemala", "Guatemala") | |
, (Hidden, "Iceland", "Iceland") | |
, (Hidden, "Iran", "Iran") | |
, (Hidden, "Kazakhstan", "Kazakhstan") | |
, (Hidden, "Lebanon", "Lebanon") | |
, (Hidden, "Malta", "Malta") | |
, (Hidden, "Nepal", "Nepal") | |
, (Hidden, "Nigeria", "Nigeria") | |
, (Hidden, "Pakistan", "Pakistan") | |
, (Hidden, "Paraguay", "Paraguay") | |
, (Hidden, "Peru", "Peru") | |
, (Hidden, "Philippines", "Philippines") | |
, (Hidden, "Serbia", "Serbia") | |
, (Hidden, "Slovakia", "Slovakia") | |
, (Hidden, "Slovenia", "Slovenia") | |
, (Hidden, "South Korea", "South Korea") | |
, (Hidden, "Syria", "Syria") | |
, (Hidden, "Uganda", "Uganda") | |
, (Hidden, "Vietnam", "Vietnam") | |
] | |
makeSingleBar | |
responseQ96 | |
96 | |
"How old are you?" | |
[ (Shown, "<18", "Under 18 years old") | |
, (Shown, "18-24", "18 to 24 years old") | |
, (Shown, "25-34", "25 to 34 years old") | |
, (Shown, "35-44", "35 to 44 years old") | |
, (Shown, "45-54", "45 to 54 years old") | |
, (Shown, "55-64", "55 to 64 years old") | |
, (Shown, ">=65", "65 years or older") | |
] | |
makeSingleBar | |
responseQ97 | |
97 | |
"What is your gender?" | |
[ (Shown, "Male", "Male") | |
, (Shown, "Female", "Female") | |
, (Shown, "Non-binary", "Non-binary") | |
] | |
makeSingleBar | |
responseQ98 | |
98 | |
"Do you identify as transgender?" | |
[(Shown, "No", "No"), (Shown, "Yes", "Yes")] | |
makeSingleBar | |
responseQ99 | |
99 | |
"Are you a student?" | |
[ (Shown, "No", "No") | |
, (Shown, "Yes, full time", "Yes, full time") | |
, (Shown, "Yes, part time", "Yes, part time") | |
] | |
makeSingleBar | |
responseQ100 | |
100 | |
"What is the highest level of education you have completed?" | |
[ ( Hidden | |
, "Less than high school diploma" | |
, "Less than high school diploma" | |
) | |
, (Shown, "High school", "High school diploma") | |
, (Shown, "Some college", "Some college") | |
, (Shown, "Associate", "Associate degree") | |
, (Shown, "Bachelor's", "Bachelor's degree") | |
, (Shown, "Master's", "Master's degree") | |
, (Shown, "Professional", "Professional degree") | |
, (Shown, "Doctoral", "Doctoral degree") | |
] | |
makeSingleBar | |
responseQ101 | |
101 | |
"What is your employment status?" | |
[ (Shown, "Full time", "Employed full time") | |
, (Shown, "Self employed", "Self employed") | |
, (Shown, "Part time", "Employed part time") | |
, (Shown, "Looking", "Not employed, but looking for work") | |
, (Shown, "Not looking", "Not employed, and not looking for work") | |
, (Shown, "Retired", "Retired") | |
] | |
makeSingleBar | |
responseQ102 | |
102 | |
"How large is the company you work for?" | |
[ (Shown, "<10", "Fewer than 10 employees") | |
, (Shown, "10-99", "10 to 99 employees") | |
, (Shown, "100-999", "100 to 999 employees") | |
, (Shown, ">=1000", "1,000 or more employees") | |
] | |
makeSingleBar | |
responseQ103 | |
103 | |
"How many years have you been coding?" | |
[ (Shown, "<5", "0 to 4 years") | |
, (Shown, "5-9", "5 to 9 years") | |
, (Shown, "10-14", "10 to 14 years") | |
, (Shown, "15-19", "15 to 19 years") | |
, (Shown, "20-24", "20 to 24 years") | |
, (Shown, "25-29", "25 to 29 years") | |
, (Shown, ">=30", "30 or more years") | |
] | |
makeSingleBar | |
responseQ104 | |
104 | |
"How many years have you been coding professionally?" | |
[ (Shown, "<5", "0 to 4 years") | |
, (Shown, "5-9", "5 to 9 years") | |
, (Shown, "10-19", "10 to 19 years") | |
, (Shown, "20-29", "20 to 29 years") | |
, (Shown, ">=30", "30 or more years") | |
] | |
makeSingleBar | |
responseQ105 | |
105 | |
"Do you code as a hobby?" | |
[(Shown, "Yes", "Yes"), (Shown, "No", "No")] | |
makeSingleBar | |
responseQ106 | |
106 | |
"Have you contributed to any open source projects?" | |
[(Shown, "Yes", "Yes"), (Shown, "No", "No")] | |
makeRatingBar responseQ107 107 "Meta survey" | |
makeSingleBar | |
responseQ108 | |
108 | |
"Did you take last year's survey?" | |
[ (Shown, "No", "No") | |
, (Shown, "Yes", "Yes") | |
, (Shown, "I don't remember", "I don't remember") | |
] | |
makeSingleBar | |
responseQ109 | |
109 | |
"How did you hear about this survey?" | |
[ (Shown, "Reddit", "Reddit") | |
, (Shown, "Twitter", "Twitter") | |
, (Shown, "Mailing list", "Mailing list") | |
, (Shown, "Haskell Weekly", "Haskell Weekly") | |
, (Shown, "Lobsters", "Lobsters") | |
, (Shown, "Slack", "Slack") | |
, (Hidden, "Discord", "Discord") | |
, (Shown, "Other", "Other") | |
, (Shown, "In person", "In person") | |
, (Hidden, "IRC", "IRC") | |
, (Hidden, "Telegram", "Telegram") | |
, (Hidden, "Gitter", "Gitter") | |
, (Hidden, "Matrix/Riot", "Matrix/Riot") | |
, (Hidden, "Mastodon", "Mastodon") | |
] | |
IO.hPutStrLn handle "</body>" | |
IO.hPutStrLn handle "</html>" | |
IO.hFlush handle | |
data Response = Response | |
{ responseQ0 :: Int | |
, responseQ1 :: Date | |
, responseQ2 :: Maybe Int | |
, responseQ3 :: Maybe Text.Text | |
, responseQ4 :: Maybe Text.Text | |
, responseQ5 :: Multiple Text.Text | |
, responseQ6 :: Maybe Text.Text | |
, responseQ7 :: Maybe Text.Text | |
, responseQ8 :: Maybe Text.Text | |
, responseQ9 :: Maybe Text.Text | |
, responseQ10 :: Multiple Text.Text | |
, responseQ11 :: Maybe Text.Text | |
, responseQ12 :: Multiple Text.Text | |
, responseQ13 :: Maybe Text.Text | |
, responseQ14 :: Multiple Text.Text | |
, responseQ15 :: Maybe Text.Text | |
, responseQ16 :: Multiple Text.Text | |
, responseQ17 :: Maybe Text.Text | |
, responseQ18 :: Multiple Text.Text | |
, responseQ19 :: Maybe Text.Text | |
, responseQ20 :: Maybe Int | |
, responseQ21 :: Maybe Text.Text | |
, responseQ22 :: Maybe Text.Text | |
, responseQ23 :: Multiple Text.Text | |
, responseQ24 :: Maybe Text.Text | |
, responseQ25 :: Multiple Text.Text | |
, responseQ26 :: Maybe Text.Text | |
, responseQ27 :: Maybe Int | |
, responseQ28 :: Multiple Text.Text | |
, responseQ29 :: Maybe Text.Text | |
, responseQ30 :: Multiple Text.Text | |
, responseQ31 :: Maybe Text.Text | |
, responseQ32 :: Maybe Text.Text | |
, responseQ33 :: Multiple Text.Text | |
, responseQ34 :: Maybe Text.Text | |
, responseQ35 :: Multiple Text.Text | |
, responseQ36 :: Maybe Text.Text | |
, responseQ37 :: Maybe Text.Text | |
, responseQ38 :: Multiple Text.Text | |
, responseQ39 :: Maybe Text.Text | |
, responseQ40 :: Maybe Int | |
, responseQ41 :: Multiple Text.Text | |
, responseQ42 :: Maybe Text.Text | |
, responseQ43 :: Multiple Text.Text | |
, responseQ44 :: Maybe Text.Text | |
, responseQ45 :: Multiple Text.Text | |
, responseQ46 :: Maybe Text.Text | |
, responseQ47 :: Multiple Text.Text | |
, responseQ48 :: Maybe Text.Text | |
, responseQ49 :: Multiple Text.Text | |
, responseQ50 :: Maybe Text.Text | |
, responseQ51 :: Multiple Text.Text | |
, responseQ52 :: Maybe Text.Text | |
, responseQ53 :: Maybe Int | |
, responseQ54 :: Multiple Text.Text | |
, responseQ55 :: Maybe Text.Text | |
, responseQ56 :: Multiple Text.Text | |
, responseQ57 :: Maybe Text.Text | |
, responseQ58 :: Maybe Int | |
, responseQ59 :: Multiple Text.Text | |
, responseQ60 :: Maybe Text.Text | |
, responseQ61 :: Multiple Text.Text | |
, responseQ62 :: Maybe Text.Text | |
, responseQ63 :: Maybe Int | |
, responseQ64 :: Maybe Text.Text | |
, responseQ65 :: Maybe Text.Text | |
, responseQ66 :: Maybe Text.Text | |
, responseQ67 :: Maybe Text.Text | |
, responseQ68 :: Maybe Text.Text | |
, responseQ69 :: Maybe Text.Text | |
, responseQ70 :: Maybe Text.Text | |
, responseQ71 :: Maybe Text.Text | |
, responseQ72 :: Maybe Text.Text | |
, responseQ73 :: Maybe Text.Text | |
, responseQ74 :: Maybe Text.Text | |
, responseQ75 :: Maybe Text.Text | |
, responseQ76 :: Maybe Text.Text | |
, responseQ77 :: Maybe Text.Text | |
, responseQ78 :: Maybe Text.Text | |
, responseQ79 :: Maybe Text.Text | |
, responseQ80 :: Maybe Text.Text | |
, responseQ81 :: Maybe Text.Text | |
, responseQ82 :: Maybe Text.Text | |
, responseQ83 :: Maybe Text.Text | |
, responseQ84 :: Maybe Text.Text | |
, responseQ85 :: Maybe Text.Text | |
, responseQ86 :: Maybe Text.Text | |
, responseQ87 :: Maybe Text.Text | |
, responseQ88 :: Maybe Text.Text | |
, responseQ89 :: Maybe Text.Text | |
, responseQ90 :: Maybe Text.Text | |
, responseQ91 :: Maybe Text.Text | |
, responseQ92 :: Maybe Text.Text | |
, responseQ93 :: Maybe Text.Text | |
, responseQ94 :: Maybe Int | |
, responseQ95 :: Maybe Text.Text | |
, responseQ96 :: Maybe Text.Text | |
, responseQ97 :: Maybe Text.Text | |
, responseQ98 :: Maybe Text.Text | |
, responseQ99 :: Maybe Text.Text | |
, responseQ100 :: Maybe Text.Text | |
, responseQ101 :: Maybe Text.Text | |
, responseQ102 :: Maybe Text.Text | |
, responseQ103 :: Maybe Text.Text | |
, responseQ104 :: Maybe Text.Text | |
, responseQ105 :: Maybe Text.Text | |
, responseQ106 :: Maybe Text.Text | |
, responseQ107 :: Maybe Int | |
, responseQ108 :: Maybe Text.Text | |
, responseQ109 :: Maybe Text.Text | |
, responseQ110 :: Maybe Text.Text | |
} deriving (Eq, Show) | |
instance Csv.DefaultOrdered Response where | |
headerOrder = const . Csv.header $ fmap snd responseFields | |
instance Csv.FromRecord Response where | |
parseRecord record = Response | |
<$> Csv.index record 0 | |
<*> Csv.index record 1 | |
<*> Csv.index record 2 | |
<*> Csv.index record 3 | |
<*> Csv.index record 4 | |
<*> Csv.index record 5 | |
<*> Csv.index record 6 | |
<*> Csv.index record 7 | |
<*> Csv.index record 8 | |
<*> Csv.index record 9 | |
<*> Csv.index record 10 | |
<*> Csv.index record 11 | |
<*> Csv.index record 12 | |
<*> Csv.index record 13 | |
<*> Csv.index record 14 | |
<*> Csv.index record 15 | |
<*> Csv.index record 16 | |
<*> Csv.index record 17 | |
<*> Csv.index record 18 | |
<*> Csv.index record 19 | |
<*> Csv.index record 20 | |
<*> Csv.index record 21 | |
<*> Csv.index record 22 | |
<*> Csv.index record 23 | |
<*> Csv.index record 24 | |
<*> Csv.index record 25 | |
<*> Csv.index record 26 | |
<*> Csv.index record 27 | |
<*> Csv.index record 28 | |
<*> Csv.index record 29 | |
<*> Csv.index record 30 | |
<*> Csv.index record 31 | |
<*> Csv.index record 32 | |
<*> Csv.index record 33 | |
<*> Csv.index record 34 | |
<*> Csv.index record 35 | |
<*> Csv.index record 36 | |
<*> Csv.index record 37 | |
<*> Csv.index record 38 | |
<*> Csv.index record 39 | |
<*> Csv.index record 40 | |
<*> Csv.index record 41 | |
<*> Csv.index record 42 | |
<*> Csv.index record 43 | |
<*> Csv.index record 44 | |
<*> Csv.index record 45 | |
<*> Csv.index record 46 | |
<*> Csv.index record 47 | |
<*> Csv.index record 48 | |
<*> Csv.index record 49 | |
<*> Csv.index record 50 | |
<*> Csv.index record 51 | |
<*> Csv.index record 52 | |
<*> Csv.index record 53 | |
<*> Csv.index record 54 | |
<*> Csv.index record 55 | |
<*> Csv.index record 56 | |
<*> Csv.index record 57 | |
<*> Csv.index record 58 | |
<*> Csv.index record 59 | |
<*> Csv.index record 60 | |
<*> Csv.index record 61 | |
<*> Csv.index record 62 | |
<*> Csv.index record 63 | |
<*> Csv.index record 64 | |
<*> Csv.index record 65 | |
<*> Csv.index record 66 | |
<*> Csv.index record 67 | |
<*> Csv.index record 68 | |
<*> Csv.index record 69 | |
<*> Csv.index record 70 | |
<*> Csv.index record 71 | |
<*> Csv.index record 72 | |
<*> Csv.index record 73 | |
<*> Csv.index record 74 | |
<*> Csv.index record 75 | |
<*> Csv.index record 76 | |
<*> Csv.index record 77 | |
<*> Csv.index record 78 | |
<*> Csv.index record 79 | |
<*> Csv.index record 80 | |
<*> Csv.index record 81 | |
<*> Csv.index record 82 | |
<*> Csv.index record 83 | |
<*> Csv.index record 84 | |
<*> Csv.index record 85 | |
<*> Csv.index record 86 | |
<*> Csv.index record 87 | |
<*> Csv.index record 88 | |
<*> Csv.index record 89 | |
<*> Csv.index record 90 | |
<*> Csv.index record 91 | |
<*> Csv.index record 92 | |
<*> Csv.index record 93 | |
<*> Csv.index record 94 | |
<*> Csv.index record 95 | |
<*> Csv.index record 96 | |
<*> Csv.index record 97 | |
<*> Csv.index record 98 | |
<*> Csv.index record 99 | |
<*> Csv.index record 100 | |
<*> Csv.index record 101 | |
<*> Csv.index record 102 | |
<*> Csv.index record 103 | |
<*> Csv.index record 104 | |
<*> Csv.index record 105 | |
<*> Csv.index record 106 | |
<*> Csv.index record 107 | |
<*> Csv.index record 108 | |
<*> Csv.index record 109 | |
<*> Csv.index record 110 | |
instance Csv.ToNamedRecord Response where | |
toNamedRecord response = | |
Csv.namedRecord $ fmap (\(f, x) -> (x, f response)) responseFields | |
responseFields :: [(Response -> Csv.Field, Csv.Name)] | |
responseFields = fmap | |
(\(f, x) -> (f, toUtf8 x)) | |
[ (Csv.toField . responseQ0, "Number") | |
, (Csv.toField . responseQ1, "Submission date") | |
, (Csv.toField . responseQ2, "HASKELL USAGE") | |
, (Csv.toField . responseQ3, "Do you use Haskell?") | |
, ( Csv.toField . responseQ4 | |
, "If you stopped using Haskell, how long did you use it before you stopped?" | |
) | |
, (Csv.toField . responseQ5, "If you do not use Haskell, why not?") | |
, ( Csv.toField . responseQ6 | |
, "Are there any other reasons why you do not use Haskell?" | |
) | |
, (Csv.toField . responseQ7, "How long have you been using Haskell?") | |
, (Csv.toField . responseQ8, "How frequently do you use Haskell?") | |
, ( Csv.toField . responseQ9 | |
, "How would you rate your proficiency in Haskell?" | |
) | |
, (Csv.toField . responseQ10, "Where do you use Haskell?") | |
, (Csv.toField . responseQ11, "Do you use Haskell at work?") | |
, (Csv.toField . responseQ12, "If you do not use Haskell at work, why not?") | |
, ( Csv.toField . responseQ13 | |
, "Are there other reasons why you do not use Haskell at work?" | |
) | |
, ( Csv.toField . responseQ14 | |
, "Which programming languages other than Haskell are you fluent in?" | |
) | |
, ( Csv.toField . responseQ15 | |
, "What other programming languages are you fluent in?" | |
) | |
, ( Csv.toField . responseQ16 | |
, "Which types of software do you develop with Haskell?" | |
) | |
, ( Csv.toField . responseQ17 | |
, "What other types of software do you develop with Haskell?" | |
) | |
, (Csv.toField . responseQ18, "Which industries do you use Haskell in?") | |
, (Csv.toField . responseQ19, "What other industries do you use Haskell in?") | |
, (Csv.toField . responseQ20, "PROJECTS") | |
, ( Csv.toField . responseQ21 | |
, "How many Haskell projects do you contribute to?" | |
) | |
, ( Csv.toField . responseQ22 | |
, "What is the total size of all the Haskell projects you contribute to?" | |
) | |
, (Csv.toField . responseQ23, "Which platforms do you develop Haskell on?") | |
, ( Csv.toField . responseQ24 | |
, "What other platforms do you develop Haskell on?" | |
) | |
, (Csv.toField . responseQ25, "Which platforms do you target?") | |
, (Csv.toField . responseQ26, "What other platforms do you target?") | |
, (Csv.toField . responseQ27, "COMPILERS") | |
, (Csv.toField . responseQ28, "Which Haskell compilers do you use?") | |
, (Csv.toField . responseQ29, "What other Haskell compilers do you use?") | |
, ( Csv.toField . responseQ30 | |
, "Which installation methods do you use for your Haskell compiler?" | |
) | |
, ( Csv.toField . responseQ31 | |
, "What other installation methods do you use for your Haskell compiler?" | |
) | |
, ( Csv.toField . responseQ32 | |
, "Has upgrading your Haskell compiler broken your code in the last year?" | |
) | |
, ( Csv.toField . responseQ33 | |
, "How has upgrading your Haskell compiler broken your code in the past year?" | |
) | |
, ( Csv.toField . responseQ34 | |
, "How else has upgrading your Haskell compiler broken your code in the past year?" | |
) | |
, (Csv.toField . responseQ35, "Which versions of GHC do you use?") | |
, ( Csv.toField . responseQ36 | |
, "How do you feel about the new GHC release schedule?" | |
) | |
, ( Csv.toField . responseQ37 | |
, "Why do you feel the way that you do about the new GHC release schedule?" | |
) | |
, ( Csv.toField . responseQ38 | |
, "Which GHC language extensions would you like to be enabled by default?" | |
) | |
, ( Csv.toField . responseQ39 | |
, "How important do you feel it would be to have a new version of the Haskell standard?" | |
) | |
, (Csv.toField . responseQ40, "TOOLING") | |
, (Csv.toField . responseQ41, "Which build tools do you use for Haskell?") | |
, ( Csv.toField . responseQ42 | |
, "What other build tools do you use for Haskell?" | |
) | |
, (Csv.toField . responseQ43, "Which editors do you use for Haskell?") | |
, (Csv.toField . responseQ44, "What other editors do you use for Haskell?") | |
, ( Csv.toField . responseQ45 | |
, "Which version control systems do you use for Haskell?" | |
) | |
, ( Csv.toField . responseQ46 | |
, "Which other version control systems do you use for Haskell?" | |
) | |
, (Csv.toField . responseQ47, "Where do you get Haskell packages from?") | |
, (Csv.toField . responseQ48, "Where else do you get Haskell packages from?") | |
, ( Csv.toField . responseQ49 | |
, "Which libraries do you use to test Haskell code?" | |
) | |
, ( Csv.toField . responseQ50 | |
, "What other tools do you use to test Haskell code?" | |
) | |
, ( Csv.toField . responseQ51 | |
, "Which libraries do you use to benchmark Haskell code?" | |
) | |
, ( Csv.toField . responseQ52 | |
, "What other tools do you use to benchmark Haskell code?" | |
) | |
, (Csv.toField . responseQ53, "INFRASTRUCTURE") | |
, ( Csv.toField . responseQ54 | |
, "Which tools do you use to deploy Haskell applications?" | |
) | |
, ( Csv.toField . responseQ55 | |
, "What other tools do you use to deploy Haskell applications?" | |
) | |
, (Csv.toField . responseQ56, "Where do you deploy Haskell applications?") | |
, ( Csv.toField . responseQ57 | |
, "Where else do you deploy Haskell applications?" | |
) | |
, (Csv.toField . responseQ58, "COMMUNITY") | |
, ( Csv.toField . responseQ59 | |
, "Where do you interact with the Haskell community?" | |
) | |
, ( Csv.toField . responseQ60 | |
, "Where else do you interact with the Haskell community?" | |
) | |
, ( Csv.toField . responseQ61 | |
, "Which of the following Haskell topics would you like to see more written about?" | |
) | |
, ( Csv.toField . responseQ62 | |
, "What other Haskell topics would you like to see more written about?" | |
) | |
, (Csv.toField . responseQ63, "FEELINGS") | |
, (Csv.toField . responseQ64, "I feel welcome in the Haskell community.") | |
, (Csv.toField . responseQ65, "I am satisfied with Haskell as a language.") | |
, ( Csv.toField . responseQ66 | |
, "What would you change about Haskell the language?" | |
) | |
, ( Csv.toField . responseQ67 | |
, "I am satisfied with Haskell's compilers, such as GHC." | |
) | |
, ( Csv.toField . responseQ68 | |
, "What would you change about Haskell's compilers?" | |
) | |
, ( Csv.toField . responseQ69 | |
, "I am satisfied with Haskell's build tools, such as Cabal." | |
) | |
, ( Csv.toField . responseQ70 | |
, "What would you change about Haskell's build tools?" | |
) | |
, ( Csv.toField . responseQ71 | |
, "I am satisfied with Haskell's package repositories, such as Hackage." | |
) | |
, ( Csv.toField . responseQ72 | |
, "What would you change about Haskell's package repositories?" | |
) | |
, ( Csv.toField . responseQ73 | |
, "I can find Haskell libraries for the things that I need." | |
) | |
, (Csv.toField . responseQ74, "I think Haskell libraries are high quality.") | |
, ( Csv.toField . responseQ75 | |
, "I have a good understanding of Haskell best practices." | |
) | |
, ( Csv.toField . responseQ76 | |
, "I think Haskell libraries are well documented." | |
) | |
, ( Csv.toField . responseQ77 | |
, "I can easily compare competing Haskell libraries to select the best one." | |
) | |
, ( Csv.toField . responseQ78 | |
, "I think that Haskell libraries are easy to use." | |
) | |
, ( Csv.toField . responseQ79 | |
, "I think that Haskell libraries provide a stable API." | |
) | |
, ( Csv.toField . responseQ80 | |
, "I think that Haskell libraries work well together." | |
) | |
, ( Csv.toField . responseQ81 | |
, "I think that software written in Haskell is easy to maintain." | |
) | |
, ( Csv.toField . responseQ82 | |
, "Once my Haskell program compiles, it generally does what I intended." | |
) | |
, (Csv.toField . responseQ83, "I think that Haskell libraries perform well.") | |
, (Csv.toField . responseQ84, "Haskell's performance meets my needs.") | |
, ( Csv.toField . responseQ85 | |
, "I can easily reason about the performance of my Haskell code." | |
) | |
, (Csv.toField . responseQ86, "I would recommend using Haskell to others.") | |
, ( Csv.toField . responseQ87 | |
, "I would prefer to use Haskell for my next new project." | |
) | |
, (Csv.toField . responseQ88, "Haskell is working well for my team.") | |
, (Csv.toField . responseQ89, "Haskell is critical to my company's success.") | |
, ( Csv.toField . responseQ90 | |
, "As a candidate, I can easily find Haskell jobs." | |
) | |
, ( Csv.toField . responseQ91 | |
, "As a hiring manager, I can easily find qualified Haskell candidates." | |
) | |
, (Csv.toField . responseQ92, "What is your favorite thing about Haskell?") | |
, ( Csv.toField . responseQ93 | |
, "What is your least favorite thing about Haskell?" | |
) | |
, (Csv.toField . responseQ94, "DEMOGRAPHICS") | |
, (Csv.toField . responseQ95, "Which country do you live in?") | |
, (Csv.toField . responseQ96, "How old are you?") | |
, (Csv.toField . responseQ97, "What is your gender?") | |
, (Csv.toField . responseQ98, "Do you identify as transgender?") | |
, (Csv.toField . responseQ99, "Are you a student?") | |
, ( Csv.toField . responseQ100 | |
, "What is the highest level of education you have completed?" | |
) | |
, (Csv.toField . responseQ101, "What is your employment status?") | |
, (Csv.toField . responseQ102, "How large is the company you work for?") | |
, (Csv.toField . responseQ103, "How many years have you been coding?") | |
, ( Csv.toField . responseQ104 | |
, "How many years have you been coding professionally?" | |
) | |
, (Csv.toField . responseQ105, "Do you code as a hobby?") | |
, ( Csv.toField . responseQ106 | |
, "Have you contributed to any open source projects?" | |
) | |
, (Csv.toField . responseQ107, "META SURVEY") | |
, (Csv.toField . responseQ108, "Did you take last year's survey?") | |
, (Csv.toField . responseQ109, "How did you hear about this survey?") | |
, ( Csv.toField . responseQ110 | |
, "Do you have any feedback about the survey itself?" | |
) | |
] | |
newtype Date | |
= Date Time.Day | |
deriving (Eq, Show) | |
instance Csv.FromField Date where | |
parseField field = do | |
string <- Csv.parseField field | |
Date <$> Time.parseTimeM False Time.defaultTimeLocale "%Y-%m-%d" string | |
instance Csv.ToField Date where | |
toField = Csv.toField . Time.formatTime Time.defaultTimeLocale "%Y-%m-%d" . dateToDay | |
dateToDay :: Date -> Time.Day | |
dateToDay (Date day) = day | |
newtype Multiple a | |
= Multiple (Set.Set a) | |
deriving (Eq, Show) | |
instance (Csv.FromField a, Ord a) => Csv.FromField (Multiple a) where | |
parseField field = do | |
records <- | |
either fail pure . Csv.decode Csv.NoHeader $ LazyByteString.fromStrict | |
field | |
record <- case Vector.toList records of | |
[] -> pure Vector.empty | |
[record] -> pure record | |
_ -> fail $ "too many records: " ++ show records | |
Multiple . Set.fromList . Vector.toList <$> traverse Csv.parseField record | |
instance Csv.ToField a => Csv.ToField (Multiple a) where | |
toField = Csv.toField . Csv.encode . pure . Set.toAscList . multipleToSet | |
multipleToSet :: Multiple a -> Set.Set a | |
multipleToSet (Multiple set) = set | |
data Visibility | |
= Hidden | |
| Shown | |
deriving (Eq, Show) | |
contains :: String -> Multiple Text.Text -> Bool | |
contains = containsAny . pure | |
containsAny :: [String] -> Multiple Text.Text -> Bool | |
containsAny xs ys = any (\x -> Set.member (Text.pack x) (multipleToSet ys)) xs | |
count :: (a -> Bool) -> Vector.Vector a -> Int | |
count predicate = Vector.foldr | |
(\element total -> if predicate element then total + 1 else total) | |
0 | |
escape :: String -> String | |
escape = concatMap escapeChar | |
escapeChar :: Char -> String | |
escapeChar c = case c of | |
'\'' -> "'" | |
'"' -> """ | |
'&' -> "&" | |
'<' -> "<" | |
'>' -> ">" | |
_ -> [c] | |
is :: String -> Maybe Text.Text -> Bool | |
is string field = field == Just (Text.pack string) | |
makeBarWith | |
:: IO.Handle | |
-> Diagrams.FileOptions | |
-> Vector.Vector Response | |
-> (Response -> a) | |
-> Int | |
-> String | |
-> [(Visibility, String, String, a -> Bool)] | |
-> IO () | |
makeBarWith handle options responses getField number title bins = do | |
let | |
path = FilePath.combine outputDirectory | |
$ FilePath.addExtension (Printf.printf "question-%03d" number) "svg" | |
fields = fmap getField responses | |
bars = fmap (\(v, s, l, p) -> (v, s, l, count p fields)) bins | |
Printf.printf "- [%s](#question-%03d)\n" title number | |
let total = fromIntegral $ Vector.length responses :: Double | |
IO.hPutStr handle $ Printf.printf "\n<h2 id='question-%03d'>" number | |
IO.hPutStr handle $ Printf.printf "<a href='#question-%03d'>#</a> " number | |
IO.hPutStr handle $ escape title | |
IO.hPutStr handle " <a href='#'>^</a>" | |
IO.hPutStrLn handle "</h2>" | |
IO.hPutStr handle $ Printf.printf "<a href='question-%03d.svg'>" number | |
IO.hPutStr handle | |
. uncurry | |
(Printf.printf | |
"<img src='question-%03d.svg' alt='' width='%f' height='%f'>" | |
number | |
) | |
$ Chart.view Diagrams.fo_size options | |
IO.hPutStrLn handle "</a>" | |
IO.hPutStrLn handle "<table>" | |
IO.hPutStrLn handle "<thead>" | |
IO.hPutStr handle "<tr>" | |
IO.hPutStr handle "<th>Answer</th>" | |
IO.hPutStr handle "<th>Count</th>" | |
IO.hPutStr handle "<th>Percent</th>" | |
IO.hPutStr handle "</tr>" | |
IO.hPutStrLn handle "</thead>" | |
IO.hPutStrLn handle "<tbody>" | |
Monad.forM_ bars $ \(_, _, l, c) -> do | |
IO.hPutStr handle "<tr>" | |
IO.hPutStr handle . Printf.printf "<td>%s</td>" $ escape l | |
IO.hPutStr handle $ Printf.printf "<td>%d</td>" c | |
IO.hPutStr handle | |
. Printf.printf "<td>%.1f%%</td>" | |
$ 100 | |
* fromIntegral c | |
/ total | |
IO.hPutStrLn handle "</tr>" | |
IO.hPutStrLn handle "</tbody>" | |
IO.hPutStrLn handle "</table>" | |
Diagrams.toFile options path $ do | |
Chart.assign Chart.layout_title title | |
Chart.setColors . pure . Chart.opaque $ Color.sRGB24 0x5c 0x35 0x66 | |
Chart.assign (Chart.layout_x_axis . Chart.laxis_generate) | |
. Chart.autoIndexAxis | |
$ Maybe.mapMaybe | |
(\(v, s, _, _) -> case v of | |
Hidden -> Nothing | |
Shown -> Just s | |
) | |
bars | |
Chart.plot | |
. fmap Chart.plotBars | |
. Chart.bars [""] | |
. Chart.addIndexes | |
. fmap pure | |
$ Maybe.mapMaybe | |
(\(v, _, _, c) -> case v of | |
Hidden -> Nothing | |
Shown -> Just c | |
) | |
bars | |
makeLikertBarWith | |
:: IO.Handle | |
-> Diagrams.FileOptions | |
-> Vector.Vector Response | |
-> (Response -> Maybe Text.Text) | |
-> Int | |
-> String | |
-> IO () | |
makeLikertBarWith handle options responses getField number title = | |
makeBarWith handle options responses getField number title $ fmap | |
(\x -> (Shown, x, x, is x)) | |
["Strongly disagree", "Disagree", "Neutral", "Agree", "Strongly agree"] | |
makeMultipleBarWith | |
:: IO.Handle | |
-> Diagrams.FileOptions | |
-> Vector.Vector Response | |
-> (Response -> Multiple Text.Text) | |
-> Int | |
-> String | |
-> [(Visibility, String, String)] | |
-> IO () | |
makeMultipleBarWith handle options responses getField number title = | |
makeBarWith | |
handle | |
options | |
responses | |
getField | |
number | |
(Printf.printf "%s (multiple select)" title) | |
. fmap | |
(\(visibility, name, value) -> | |
(visibility, name, value, contains value) | |
) | |
makeRatingBarWith | |
:: IO.Handle | |
-> Diagrams.FileOptions | |
-> Vector.Vector Response | |
-> (Response -> Maybe Int) | |
-> Int | |
-> String | |
-> IO () | |
makeRatingBarWith handle options responses getField number title = | |
makeBarWith handle options responses getField number title $ fmap | |
(\n -> | |
( Shown | |
, show n | |
, Printf.printf "%d heart%s" n (if n == 1 then "" else "s") | |
, (== Just n) | |
) | |
) | |
[1 .. 10] | |
makeSingleBarWith | |
:: IO.Handle | |
-> Diagrams.FileOptions | |
-> Vector.Vector Response | |
-> (Response -> Maybe Text.Text) | |
-> Int | |
-> String | |
-> [(Visibility, String, String)] | |
-> IO () | |
makeSingleBarWith handle options responses getField number title = | |
makeBarWith | |
handle | |
options | |
responses | |
getField | |
number | |
(Printf.printf "%s (single select)" title) | |
. fmap (\(visibility, name, value) -> (visibility, name, value, is value)) | |
outputDirectory :: FilePath | |
outputDirectory = "output" | |
toUtf8 :: String -> ByteString.ByteString | |
toUtf8 = Encoding.encodeUtf8 . Text.pack |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment