moduleSolutions.DPairimportControl.Monad.StateimportData.DPairimportData.EitherimportData.HListimportData.ListimportData.List1importData.SingletonimportData.StringimportData.VectimportText.CSVimportSystem.File%defaulttotal---------------------------------------------------------------------------------- Dependent Pairs---------------------------------------------------------------------------------- 1filterVect:(a->Bool)->Vectma-> (n**Vectna)
filterVectf[]= (_**[])
filterVectf(x::xs)=casefxofTrue=>let (_**ys) =filterVectfxsin (_**x::ys)
False=>filterVectfxs-- 2mapMaybeVect:(a->Maybeb)->Vectma-> (n**Vectnb)
mapMaybeVectf[]= (_**[])
mapMaybeVectf(x::xs)=casefxofJustv=>let (_**vs) =mapMaybeVectfxsin (_**v::vs)
Nothing=>mapMaybeVectfxs-- 3dropWhileVect:(a->Bool)->Vectma->Exists(\n=>Vectna)dropWhileVectf[]=Evidence_[]dropWhileVectf(x::xs)=casefxofTrue=>dropWhileVectfxsFalse=>Evidence_(x::xs)-- 4vectLength:Vectna->SingletonnvectLength[]=Val0vectLength(x::xs)=letValk=vectLengthxsinVal(Sk)dropWhileVect':(a->Bool)->Vectma-> (n**Vectna)
dropWhileVect'fxs=letEvidence_ys=dropWhileVectfxsValn=vectLengthysin (n**ys)
---------------------------------------------------------------------------------- Use Case: Nucleic Acids---------------------------------------------------------------------------------- 1dataBaseType=DNABase|RNABasedataNucleobase':BaseType->TypewhereAdenine':Nucleobase'bCytosine':Nucleobase'bGuanine':Nucleobase'bThymine':Nucleobase'DNABaseUracile':Nucleobase'RNABaseRNA':TypeRNA'=List(Nucleobase'RNABase)DNA':TypeDNA'=List(Nucleobase'DNABase)Acid1:TypeAcid1= (b**List(Nucleobase'b))
recordAcid2whereconstructorMkAcid2baseType:BaseTypesequence:List(Nucleobase'baseType)dataAcid3:TypewhereSomeRNA:RNA'->Acid3SomeDNA:DNA'->Acid3nb12:Acid1->Acid2nb12 (fst**snd) =MkAcid2fstsndnb21:Acid2->Acid1nb21(MkAcid2btseq)= (bt**seq)
nb13:Acid1->Acid3nb13 (DNABase**snd) =SomeDNAsndnb13 (RNABase**snd) =SomeRNAsndnb31:Acid3->Acid1nb31(SomeRNAxs)= (RNABase**xs)
nb31(SomeDNAxs)= (DNABase**xs)
-- 2dataDir=Sense|AntisensedataNucleobase:BaseType->Dir->TypewhereAdenine:NucleobasebdCytosine:NucleobasebdGuanine:NucleobasebdThymine:NucleobaseDNABasedUracile:NucleobaseRNABasedRNA:Dir->TypeRNAd=List(NucleobaseRNABased)DNA:Dir->TypeDNAd=List(NucleobaseDNABased)-- 3inverse:Dir->DirinverseSense=AntisenseinverseAntisense=SensecomplementBase:(b:BaseType)->Nucleobasebdir->Nucleobaseb(inversedir)complementBaseDNABaseAdenine=ThyminecomplementBaseRNABaseAdenine=UracilecomplementBase_Cytosine=GuaninecomplementBase_Guanine=CytosinecomplementBase_Thymine=AdeninecomplementBase_Uracile=Adeninecomplement:(b:BaseType)->List(Nucleobasebdir)->List(Nucleobaseb $ inversedir)complementb=map(complementBaseb)transcribeBase:NucleobaseDNABaseAntisense->NucleobaseRNABaseSensetranscribeBaseAdenine=UraciletranscribeBaseCytosine=GuaninetranscribeBaseGuanine=CytosinetranscribeBaseThymine=Adeninetranscribe:DNAAntisense->RNASensetranscribe=maptranscribeBasetranscribeAny:(dir:Dir)->DNAdir->RNASensetranscribeAnyAntisense=transcribetranscribeAnySense=transcribe.complement_-- 4recordNucleicAcidwhereconstructorMkNucleicAcidbaseType:BaseTypedir:Dirsequence:List(NucleobasebaseTypedir)-- 5readAnyBase:{0dir:_}->Char->Maybe(Nucleobasebdir)readAnyBase'A'=JustAdeninereadAnyBase'C'=JustCytosinereadAnyBase'G'=JustGuaninereadAnyBase_=NothingreadRNABase:{0dir:_}->Char->Maybe(NucleobaseRNABasedir)readRNABase'U'=JustUracilereadRNABasec=readAnyBasecreadDNABase:{0dir:_}->Char->Maybe(NucleobaseDNABasedir)readDNABase'T'=JustThyminereadDNABasec=readAnyBasecreadRNA:String->Maybe(dir:Dir**RNAdir)readRNAstr=caseforget $ split('-'==)strof["5´",s,"3´"]=>MkDPairSense<$>traversereadRNABase(unpacks)["3´",s,"5´"]=>MkDPairAntisense<$>traversereadRNABase(unpacks)_=>NothingreadDNA:String->Maybe(dir:Dir**DNAdir)readDNAstr=caseforget $ split('-'==)strof["5´",s,"3´"]=>MkDPairSense<$>traversereadDNABase(unpacks)["3´",s,"5´"]=>MkDPairAntisense<$>traversereadDNABase(unpacks)_=>Nothing-- 6preSuf:Dir->(String,String)preSufSense=("5´-","-3´")preSufAntisense=("3´-","-5´")encodeBase:Nucleobasecd->CharencodeBaseAdenine='A'encodeBaseCytosine='C'encodeBaseGuanine='G'encodeBaseThymine='T'encodeBaseUracile='U'encode:(dir:Dir)->List(Nucleobasebdir)->Stringencodedirseq=let(pre,suf)=preSufdirinpre++pack(mapencodeBaseseq)++suf-- 7publicexportdataInputError:TypewhereUnknownBaseType:String->InputErrorInvalidSequence:String->InputErrorreadAcid:(b:BaseType)->String->EitherInputError (d**List $ Nucleobasebd)
readAcidbstr=leterr=InvalidSequencestrincasebofDNABase=>maybeToEithererr $ readDNAstrRNABase=>maybeToEithererr $ readRNAstrtoAcid:(b:BaseType)-> (d**List $ Nucleobasebd) ->NucleicAcidtoAcidb (d**seq) =MkNucleicAcidbdseqgetNucleicAcid:IO(EitherInputErrorNucleicAcid)getNucleicAcid=dobaseString<-getLinecasebaseStringof"DNA"=>map(toAcid_).readAcidDNABase<$>getLine"RNA"=>map(toAcid_).readAcidRNABase<$>getLine_=>pure $ Left(UnknownBaseTypebaseString)printRNA:RNASense->IO()printRNA=putStrLn.encode_transcribeProg:IO()transcribeProg=doRight(MkNucleicAcidbdseq)<-getNucleicAcid|Left(InvalidSequencestr)=>putStrLn $ "Invalid sequence: "++str|Left(UnknownBaseTypestr)=>putStrLn $ "Unknown base type: "++strcasebofDNABase=>printRNA $ transcribeAnydseqRNABase=>casedofSense=>printRNAseqAntisense=>printRNA $ complement_seq---------------------------------------------------------------------------------- Use Case: CSV Files with a Schema---------------------------------------------------------------------------------- A lot of code was copy-pasted from the chapter's text and is, therefore-- not very interesting. I tried to annotate the new parts with some hints-- for better understanding. Also, instead of grouping code by exercise number,-- I organized it thematically.-- *** Types ***-- I used an indexed type here to make sure, data-- constructor `Optional` takes only non-nullary types-- as arguments. As noted in exercise 3, having a nesting-- of nullary types does not make sense without a way to-- distinguish between a `Nothing` and a `Just Nothing`,-- both of which would be encoded as the empty string.-- For `Finite`, we have to add `n` as an argument to the-- data constructor, so we can use it to decode values-- of type `Fin n`.dataColType0:(nullary:Bool)->TypewhereI64:ColType0bStr:ColType0bBoolean:ColType0bFloat:ColType0bNatural:ColType0bBigInt:ColType0bFinite:Nat->ColType0bOptional:ColType0False->ColType0True-- This is the type used in schemata, where nullary types-- are explicitly allowed.ColType:TypeColType=ColType0TrueSchema:TypeSchema=ListColType-- The only interesting new parts are the last two-- lines. They should be pretty self-explanatory.IdrisType:ColType0b->TypeIdrisTypeI64=Int64IdrisTypeStr=StringIdrisTypeBoolean=BoolIdrisTypeFloat=DoubleIdrisTypeNatural=NatIdrisTypeBigInt=IntegerIdrisType(Finiten)=FinnIdrisType(Optionalt)=Maybe $ IdrisTypetRow:Schema->TypeRow=HList.mapIdrisTyperecordTablewhereconstructorMkTableschema:Schemasize:Natrows:Vectsize(Rowschema)dataError:TypewhereExpectedEOI:(pos:Nat)->String->ErrorExpectedLine:ErrorInvalidCell:(row,col:Nat)->ColType0b->String->ErrorNoNat:String->ErrorOutOfBounds:(size:Nat)->(index:Nat)->ErrorReadError:(path:String)->FileError->ErrorUnexpectedEOI:(pos:Nat)->String->ErrorUnknownCommand:String->ErrorUnknownType:(pos:Nat)->String->ErrorWriteError:(path:String)->FileError->Error-- Oh, the type of `Query` is a nice one. :-)-- `PrintTable`, on the other hand, is trivial.-- The save and load commands are special: They will-- already have carried out their tasks after parsing.-- This allow us to keep `applyCommand` pure.dataCommand:(t:Table)->TypewherePrintSchema:CommandtPrintSize:CommandtPrintTable:CommandtLoad:Table->CommandtSave:CommandtNew:(newSchema:Schema)->CommandtPrepend:Row(schemat)->CommandtGet:Fin(sizet)->CommandtDelete:Fin(sizet)->CommandtQuit:CommandtQuery:(ix:Fin(length $ schemat))->(val:IdrisType $ indexList(schemat)ix)->Commandt-- *** Core Functionality ***-- Compares two values for equality.eq:(c:ColType0b)->IdrisTypec->IdrisTypec->BooleqI64xy=x==yeqStrxy=x==yeqBooleanxy=x==yeqFloatxy=x==yeqNaturalxy=x==yeqBigIntxy=x==yeq(Finitek)xy=x==yeq(Optionalz)(Justx)(Justy)=eqzxyeq(Optionalz)NothingNothing=Trueeq(Optionalz)__=False-- Note: It would have been quite a bit easier to type and-- implement this, had we used a heterogeneous vector instead-- of a heterogeneous list for encoding table rows. However,-- I still think it's pretty cool that this type checks!eqAt:(ts:Schema)->(ix:Fin $ lengthts)->(val:IdrisType $ indexListtsix)->(row:Rowts)->BooleqAt(x::_)FZval(v::_)=eqxvalveqAt(_::xs)(FSy)val(_::vs)=eqAtxsyvalvseqAt[]___impossible-- Most new commands don't change the table,-- so their cases are trivial. The exception is-- `Load`, which replaces the table completely.applyCommand:(t:Table)->Commandt->TableapplyCommandtPrintSchema=tapplyCommandtPrintSize=tapplyCommandtPrintTable=tapplyCommandtSave=tapplyCommand_(Loadt')=t'applyCommand_(Newts)=MkTablets_[]applyCommand(MkTabletsnrs)(Prependr)=MkTablets_ $ r::rsapplyCommandt(Getx)=tapplyCommandtQuit=tapplyCommandt(Queryixval)=tapplyCommand(MkTabletsnrs)(Deletex)=casenofSk=>MkTabletsk(deleteAtxrs)Z=>absurdx-- *** Parsers ***zipWithIndex:Traversablet=>ta->t(Nat,a)zipWithIndex=evalState1.traversepairWithIndexwherepairWithIndex:a->StateNat(Nat,a)pairWithIndexv=(,v)<$>get<*modifySfromCSV:String->ListStringfromCSV=forget.split(','==)-- Reads a primitive (non-nullary) type. This is therefore-- universally quantified over parameter `b`.-- The only interesting part is the parsing of `finXYZ`,-- where we `break` the string at the occurrence of-- the first digit.readPrim:Nat->String->EitherError(ColType0b)readPrim_"i64"=RightI64readPrim_"str"=RightStrreadPrim_"boolean"=RightBooleanreadPrim_"float"=RightFloatreadPrim_"natural"=RightNaturalreadPrim_"bigint"=RightBigIntreadPrimns=leterr=Left $ UnknownTypensincasebreakisDigitsof("fin",r)=>maybeerr(Right.Finite) $ parsePositiver_=>err-- This is the parser for (possibly nullary) column types.-- A nullary type is encoded as the corresponding non-nullary-- type with a question mark appended. We therefore first check-- for the presence of said question mark at the end of the string.readColType:Nat->String->EitherErrorColTypereadColTypens=casereverse(unpacks)of'?'::t=>Optional<$>readPrimn(pack $ reverset)_=>readPrimnsreadSchema:String->EitherErrorSchemareadSchema=traverse(uncurryreadColType).zipWithIndex.fromCSVreadSchemaList:ListString->EitherErrorSchemareadSchemaList[s]=readSchemasreadSchemaList_=LeftExpectedLine-- For all except nullary types we can just use the `CSVField`-- implementation for reading values.-- For values of nullary types, we treat the empty string specially.decodeF:(c:ColType0b)->String->Maybe(IdrisTypec)decodeFI64s=readsdecodeFStrs=readsdecodeFBooleans=readsdecodeFFloats=readsdecodeFNaturals=readsdecodeFBigInts=readsdecodeF(Finitek)s=readsdecodeF(Optionaly)""=JustNothingdecodeF(Optionaly)s=Just<$>decodeFysdecodeField:(row,col:Nat)->(c:ColType0b)->String->EitherError(IdrisTypec)decodeFieldrowkcs=maybeToEither(InvalidCellrowkcs) $ decodeFcsdecodeRow:{ts:_}->(row:Nat)->String->EitherError(Rowts)decodeRowrows=go1ts $ fromCSVswherego:Nat->(cs:Schema)->ListString->EitherError(Rowcs)gok[][]=Right[]gok[](_::_)=Left $ ExpectedEOIksgok(_::_)[]=Left $ UnexpectedEOIksgok(c::cs)(s::ss)=[|decodeFieldrowkcs::go(Sk)csss|]decodeRows:{ts:_}->ListString->EitherError(List $ Rowts)decodeRows=traverse(uncurrydecodeRow).zipWithIndexreadFin:{n:_}->String->EitherError(Finn)readFins=doSk<-maybeToEither(NoNats) $ parsePositive{a=Nat}s|Z=>Left $ OutOfBoundsnZmaybeToEither(OutOfBoundsn $ Sk) $ natToFinknreadCommand:(t:Table)->String->EitherError(Commandt)readCommand_"schema"=RightPrintSchemareadCommand_"size"=RightPrintSizereadCommand_"table"=RightPrintTablereadCommand_"quit"=RightQuitreadCommand(MkTabletsn_)s=casewordssof["new",str]=>New<$>readSchemastr"add"::ss=>Prepend<$>decodeRow1(unwordsss)["get",str]=>Get<$>readFinstr["delete",str]=>Delete<$>readFinstr"query"::n::ss=>doix<-readFinnval<-decodeField11(indexListtsix)(unwordsss)pure $ Queryixval_=>Left $ UnknownCommands-- *** Printers ***toCSV:ListString->StringtoCSV=concat.intersperse","-- We mark optional type by appending a question-- mark after the corresponding non-nullary type.showColType:ColType0b->StringshowColTypeI64="i64"showColTypeStr="str"showColTypeBoolean="boolean"showColTypeFloat="float"showColTypeNatural="natural"showColTypeBigInt="bigint"showColType(Finiten)="fin\{shown}"showColType(Optionalt)=showColTypet++"?"-- Again, only nullary values are treated specially. This-- is another case of a dependent pattern match: We use-- explicit pattern matches on the value to encode based-- on the type calculated from the `ColType0 b` parameter.-- There are few languages capable of expressing this as-- cleanly as Idris does.encodeField:(t:ColType0b)->IdrisTypet->StringencodeFieldI64x=showxencodeFieldStrx=xencodeFieldBooleanTrue="t"encodeFieldBooleanFalse="f"encodeFieldFloatx=showxencodeFieldNaturalx=showxencodeFieldBigIntx=showxencodeField(Finitek)x=showxencodeField(Optionaly)(Justv)=encodeFieldyvencodeField(Optionaly)Nothing=""encodeFields:(ts:Schema)->Rowts->Vect(lengthts)StringencodeFields[][]=[]encodeFields(c::cs)(v::vs)=encodeFieldcv::encodeFieldscsvsencodeTable:Table->StringencodeTable(MkTablets_rows)=unlines.toList $ map(toCSV.toList.encodeFieldsts)rowsencodeSchema:Schema->StringencodeSchema=toCSV.mapshowColType-- Pretty printing a table plus header. All cells are right-padded-- with spaces to adjust their size to the cell with the longest-- entry for each colum.-- Value `ls` is a `Vect n Nat` holding these lengths.-- Here is an example of how the output looks like:---- fin100 | boolean | natural | str | bigint?-- ---------------------------------------------------- 88 | f | 10 | stefan |-- 13 | f | 10 | hock | -100-- 58 | t | 1000 | hello world | -1234---- Ideally, numeric values would be right-aligned, but since this-- whole exercise is already quite long and complex, I refrained-- from adding this luxury.prettyTable:{n:_}->(header:VectnString)->(table:Vectm(VectnString))->StringprettyTableht=let-- vector holding the maximal length of each columnls=foldl(zipWith $ \k=>maxk.length)(replicatenZ)(h::t)-- horizontal bar used to separate the header from the rowsbar=concat.intersperse"---" $ map(`replicate`'-')lsinunlines.toList $ linelsh::bar::map(linels)twherepad:Nat->String->Stringpadv=padRightv' '-- given a vector of lengths, pads each string to the-- desired length, separating cells with a vertical bar.line:VectnNat->VectnString->Stringlinelengths=concat.intersperse" | ".zipWithpadlengthsprintTable:(cs:ListColType)->(rows:Vectn(Rowcs))->StringprintTablecsrows=letheader=mapshowColType $ fromListcstable=map(encodeFieldscs)rowsinprettyTableheadertableallTypes:StringallTypes=concat.List.intersperse", ".map(showColType{b=True})
$ [I64,Str,Boolean,Float]showError:Error->StringshowErrorExpectedLine=""" Error when reading schema. Expected a single line of content. """showError(UnknownCommandx)=""" Unknown command: \{x}. Known commands are: clear, schema, size, table, new, add, get, delete, quit. """showError(UnknownTypeposx)=""" Unknown type at position \{showpos}: \{x}. Known types are: \{allTypes}. """showError(InvalidCellrowcoltpex)=""" Invalid value at row \{showrow}, column \{showcol}. Expected type: \{showColTypetpe}. Value found: \{x}. """showError(ExpectedEOIkx)=""" Expected end of input. Position: \{showk} Input: \{x} """showError(UnexpectedEOIkx)=""" Unxpected end of input. Position: \{showk} Input: \{x} """showError(OutOfBoundssizeindex)=""" Index out of bounds. Size of table: \{showsize} Index: \{showindex} Note: Indices start at zero. """showError(WriteErrorpatherr)=""" Error when writing file \{path}. Message: \{showerr} """showError(ReadErrorpatherr)=""" Error when reading file \{path}. Message: \{showerr} """showError(NoNatx)="Not a natural number: \{x}"result:(t:Table)->Commandt->StringresulttPrintSchema="Current schema: \{encodeSchemat.schema}"resulttPrintSize="Current size: \{showt.size}"resulttPrintTable="Table:\n\n\{printTablet.schemat.rows}"result_Save="Table written to disk."result_(Loadt)="Table loaded. Schema: \{encodeSchemat.schema}"result_(Newts)="Created table. Schema: \{encodeSchemats}"resultt(Prependr)="Row prepended:\n\n\{printTablet.schema [r]}"result_(Deletex)="Deleted row: \{show $ FS x}."result_Quit="Goodbye."resultt(Queryixval)=let (_**rs) =filter(eqAtt.schemaixval)t.rowsin"Result:\n\n\{printTablet.schemars}"resultt(Getx)="Row \{show $ FS x}:\n\n\{printTablet.schema [indexxt.rows]}"-- *** File IO ***-- We use partial function `readFile` for simplicity here.partialload:(path:String)->(decode:ListString->EitherErrora)->IO(EitherErrora)loadpathdecode=doRightls<-readFilepath|Lefterr=>pure $ Left(ReadErrorpatherr)pure $ decode(filter(not.null) $ linesls)write:(path:String)->(content:String)->IO(EitherError())writepathcontent=mapFst(WriteErrorpath)<$>writeFilepathcontentnamespaceIOEitherexport(>>=):IO(Eithererra)->(a->IO(Eithererrb))->IO(Eithererrb)ioa>>=f=Prelude.(>>=)ioa(either(pure.Left)f)export(>>):IO(Eithererr())->IO(Eithererra)->IO(Eithererra)(>>)xy=x>>=constyexportpure:a->IO(Eithererra)pure=Prelude.pure.RightpartialreadCommandIO:(t:Table)->String->IO(EitherError(Commandt))readCommandIOts=casewordssof["save",pth]=> IOEither.do
write(pth++".schema")(encodeSchemat.schema)write(pth++".csv")(encodeTablet)pureSave["load",pth]=> IOEither.do
schema<-load(pth++".schema")readSchemaListrows<-load(pth++".csv")(decodeRows{ts=schema})pure.Load $ MkTableschema(lengthrows)(fromListrows)_=>Prelude.pure $ readCommandts-- *** Main Loop ***partialrunProg:Table->IO()runProgt=doputStr"Enter a command: "str<-getLinecmd<-readCommandIOtstrcasecmdofLefterr=>putStrLn(showErrorerr)>>runProgtRightQuit=>putStrLn(resulttQuit)Rightcmd=>putStrLn(resulttcmd)>>runProg(applyCommandtcmd)partialmain:IO()main=runProg $ MkTable[]_[]