initial commit
This commit is contained in:
commit
2fa209275f
9 changed files with 217 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
dist-newstyle/**
|
||||
result/**
|
||||
dist/**
|
||||
1
cabal.config
Normal file
1
cabal.config
Normal file
|
|
@ -0,0 +1 @@
|
|||
compiler: ghcjs
|
||||
3
cabal.project
Normal file
3
cabal.project
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ignore-project: False
|
||||
package zlib
|
||||
flags: -pkg-config +bundled-c-zlib
|
||||
48
client/Main.hs
Normal file
48
client/Main.hs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
-- | Haskell language pragma
|
||||
|
||||
-- | Haskell module declaration
|
||||
module Main where
|
||||
|
||||
-- | Miso framework import
|
||||
import Miso
|
||||
import Miso.String
|
||||
|
||||
-- | Type synonym for an application model
|
||||
type Model = Int
|
||||
|
||||
-- | Sum type for application events
|
||||
data Action
|
||||
= AddOne
|
||||
| SubtractOne
|
||||
| NoOp
|
||||
| SayHelloWorld
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Entry point for a miso application
|
||||
main :: IO ()
|
||||
main = startApp App {..}
|
||||
where
|
||||
initialAction = SayHelloWorld -- initial action to be executed on application load
|
||||
model = 0 -- initial model
|
||||
update = updateModel -- update function
|
||||
view = viewModel -- view function
|
||||
events = defaultEvents -- default delegated events
|
||||
subs = [] -- empty subscription list
|
||||
mountPoint = Nothing -- mount point for application (Nothing defaults to 'body')
|
||||
logLevel = Off -- used during prerendering to see if the VDOM and DOM are in sync (only used with `miso` function)
|
||||
|
||||
-- | Updates model, optionally introduces side effects
|
||||
updateModel :: Action -> Model -> Effect Action Model
|
||||
updateModel AddOne m = noEff (m + 1)
|
||||
updateModel SubtractOne m = noEff (m - 1)
|
||||
updateModel NoOp m = noEff m
|
||||
updateModel SayHelloWorld m = m <# do
|
||||
putStrLn "Hello World" >> pure NoOp
|
||||
|
||||
-- | Constructs a virtual DOM from a model
|
||||
viewModel :: Model -> View Action
|
||||
viewModel x = div_ [] [
|
||||
button_ [ onClick AddOne ] [ text "+" ]
|
||||
, text (ms x)
|
||||
, button_ [ onClick SubtractOne ] [ text "-" ]
|
||||
]
|
||||
10
default.nix
Normal file
10
default.nix
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
with (import
|
||||
(
|
||||
builtins.fetchGit {
|
||||
url = "https://github.com/dmjio/miso";
|
||||
ref = "master";
|
||||
}
|
||||
)
|
||||
{});
|
||||
|
||||
pkgs.haskell.packages.ghcjs.callCabal2nix "rcv-site" ./. {}
|
||||
83
rcv-site.cabal
Normal file
83
rcv-site.cabal
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
name: rcv-site
|
||||
version: 0.1.0.0
|
||||
category: Web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
|
||||
executable server
|
||||
main-is:
|
||||
Main.hs
|
||||
if impl(ghcjs)
|
||||
buildable: False
|
||||
else
|
||||
default-language:
|
||||
Haskell2010
|
||||
default-extensions:
|
||||
ScopedTypeVariables,
|
||||
OverloadedStrings,
|
||||
OverloadedLists,
|
||||
TemplateHaskell,
|
||||
DataKinds,
|
||||
FlexibleContexts,
|
||||
FlexibleInstances,
|
||||
MultiParamTypeClasses,
|
||||
OverloadedLabels
|
||||
TypeFamilies,
|
||||
UndecidableInstances,
|
||||
RecursiveDo,
|
||||
RecordWildCards,
|
||||
RankNTypes,
|
||||
DuplicateRecordFields,
|
||||
TupleSections,
|
||||
TypeOperators,
|
||||
DeriveGeneric
|
||||
hs-source-dirs:
|
||||
server,
|
||||
shared
|
||||
build-depends:
|
||||
base < 5,
|
||||
aeson,
|
||||
containers,
|
||||
http-types,
|
||||
lucid,
|
||||
miso,
|
||||
mtl,
|
||||
network-uri,
|
||||
servant,
|
||||
servant-lucid,
|
||||
servant-server,
|
||||
text,
|
||||
wai,
|
||||
wai-app-static,
|
||||
wai-extra,
|
||||
warp
|
||||
default-language:
|
||||
Haskell2010
|
||||
|
||||
|
||||
executable client
|
||||
main-is:
|
||||
Main.hs
|
||||
if !impl(ghcjs)
|
||||
buildable: False
|
||||
else
|
||||
default-extensions:
|
||||
ScopedTypeVariables,
|
||||
OverloadedStrings,
|
||||
OverloadedLists,
|
||||
RecordWildCards,
|
||||
DuplicateRecordFields,
|
||||
DeriveGeneric
|
||||
ghcjs-options:
|
||||
-dedupe -DGHCJS_GC_INTERVAL=5000
|
||||
hs-source-dirs:
|
||||
client,
|
||||
shared
|
||||
build-depends:
|
||||
aeson,
|
||||
base < 5,
|
||||
containers,
|
||||
miso,
|
||||
servant
|
||||
default-language:
|
||||
Haskell2010
|
||||
39
server/InstantRunoff.hs
Normal file
39
server/InstantRunoff.hs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
module InstantRunoff where
|
||||
import qualified Data.List as L
|
||||
import qualified Data.List.NonEmpty as LN
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Maybe as My
|
||||
import qualified Data.Set as S
|
||||
import Data.Ord
|
||||
import Data.Maybe
|
||||
import Data.Ratio
|
||||
|
||||
|
||||
solve :: Ord a => LN.NonEmpty (LN.NonEmpty a) -> LN.NonEmpty a
|
||||
solve votes =
|
||||
case (L.find (\(_, share) -> share > (1 % 2)) . assocs) of
|
||||
Just (winner, share) -> return winner -- singleton not introduced until base 4.15
|
||||
Nothing -> solve . remove $ S.insert noFirstChoice firstChoiceLoser
|
||||
where
|
||||
|
||||
-- fromList is partial, but inputs that would cause a faliure are caught by the case statement
|
||||
remove :: S.Set a -> LN.NonEmpty (LN.NonEmpty a)
|
||||
remove toRemove = LN.fromList . LN.filter null . LN.map (`elem` toRemove) $ votes
|
||||
|
||||
firstChoiceLoser :: a
|
||||
firstChoiceLoser = L.minimumBy (\(_, a0) (_, a1) -> compare a0 a1) . LN.map LN.head $ voteShares'
|
||||
|
||||
firstChoices :: LN.NonEmpty a
|
||||
firstChoices = LN.map LN.head $ votes
|
||||
|
||||
voteShares' :: LN.NonEmpty a -> M.Map a (Ratio Int)
|
||||
voteShares' = voteShares firstChoices
|
||||
|
||||
noFirstChoice :: S.Set a
|
||||
noFirstChoice = (S.fromList LN.toList . mconcat $ votes) S.\\ (S.fromList . LN.toList $ firstChoices)
|
||||
|
||||
allSame :: Eq a => [a] -> Bool
|
||||
allSame = (== 1) . LN.length . LN.nub
|
||||
|
||||
voteShares :: Ord a => LN.NonEmpty a -> M.Map a (Ratio Int)
|
||||
voteShares l = M.map (% (LN.length l)) . M.fromListWith (+) . map (, 1) . LN.toList $ l
|
||||
6
server/Main.hs
Normal file
6
server/Main.hs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module Main where
|
||||
import qualified InstantRunoff as IR
|
||||
import qualified API as A
|
||||
|
||||
main :: IO ()
|
||||
main = putStrLn "todo"
|
||||
24
shared/API.hs
Normal file
24
shared/API.hs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
module API where
|
||||
|
||||
import Servant.API
|
||||
import Data.Text (Text)
|
||||
import Data.Aeson
|
||||
import qualified Data.List.NonEmpty as LN
|
||||
|
||||
type Ballot = LN.NonEmpty Text
|
||||
|
||||
data Poll = Poll {
|
||||
question :: Text,
|
||||
options :: Ballot
|
||||
} deriving (Generic, Ord, Eq)
|
||||
|
||||
instance FromJSON Poll
|
||||
instance ToJSON Poll
|
||||
|
||||
instance FromJSON Ballot
|
||||
instance ToJSON Ballot
|
||||
|
||||
type RCVAPI =
|
||||
"poll" :> Capture "pollId" :> "vote" :> ReqBody '[JSON] Ballot :> Post '[JSON] ()
|
||||
<|> "poll" :> "create" :> ReqBody '[JSON] Poll :> Post '[JSON] Int
|
||||
<|> "poll" :> Capture "pollId" :> Get '[JSON] Poll
|
||||
Loading…
Add table
Add a link
Reference in a new issue