├── .gitignore ├── .paket └── paket.targets ├── README.md ├── build.cmd ├── build.fsx ├── build.sh ├── paket.dependencies ├── paket.lock └── src ├── FSharpTypeclasses ├── App.config ├── FSharpTypeclasses.fsproj ├── eq.fs ├── eq.fsi ├── functor.fs ├── functor.fsi ├── paket.references ├── to_json.fs └── to_json.fsi └── FSharpTypeclassesTest ├── App.config ├── FSharpTypeclassesTest.fsproj ├── paket.references └── to_json_test.fs /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/f#,linux,windows,macos,vim,emacs,visualstudio,visualstudiocode 2 | 3 | ### F# ### 4 | lib/debug 5 | lib/release 6 | Debug 7 | *.suo 8 | *.user 9 | *.exe 10 | 11 | ### Linux ### 12 | *~ 13 | 14 | # temporary files which can be created if a process still has a handle open of a deleted file 15 | .fuse_hidden* 16 | 17 | # KDE directory preferences 18 | .directory 19 | 20 | # Linux trash folder which might appear on any partition or disk 21 | .Trash-* 22 | 23 | 24 | ### Windows ### 25 | # Windows image file caches 26 | Thumbs.db 27 | ehthumbs.db 28 | 29 | # Folder config file 30 | Desktop.ini 31 | 32 | # Recycle Bin used on file shares 33 | $RECYCLE.BIN/ 34 | 35 | # Windows Installer files 36 | *.cab 37 | *.msi 38 | *.msm 39 | *.msp 40 | 41 | # Windows shortcuts 42 | *.lnk 43 | 44 | 45 | ### macOS ### 46 | *.DS_Store 47 | .AppleDouble 48 | .LSOverride 49 | 50 | # Icon must end with two \r 51 | Icon 52 | 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | 74 | ### Vim ### 75 | # swap 76 | [._]*.s[a-w][a-z] 77 | [._]s[a-w][a-z] 78 | # session 79 | Session.vim 80 | # temporary 81 | .netrwhist 82 | *~ 83 | # auto-generated tag files 84 | tags 85 | 86 | 87 | ### Emacs ### 88 | # -*- mode: gitignore; -*- 89 | *~ 90 | \#*\# 91 | /.emacs.desktop 92 | /.emacs.desktop.lock 93 | *.elc 94 | auto-save-list 95 | tramp 96 | .\#* 97 | 98 | # Org-mode 99 | .org-id-locations 100 | *_archive 101 | 102 | # flymake-mode 103 | *_flymake.* 104 | 105 | # eshell files 106 | /eshell/history 107 | /eshell/lastdir 108 | 109 | # elpa packages 110 | /elpa/ 111 | 112 | # reftex files 113 | *.rel 114 | 115 | # AUCTeX auto folder 116 | /auto/ 117 | 118 | # cask packages 119 | .cask/ 120 | dist/ 121 | 122 | # Flycheck 123 | flycheck_*.el 124 | 125 | # server auth directory 126 | /server/ 127 | 128 | # projectiles files 129 | .projectile 130 | 131 | ### VisualStudioCode ### 132 | .vscode 133 | 134 | 135 | 136 | ### VisualStudio ### 137 | ## Ignore Visual Studio temporary files, build results, and 138 | ## files generated by popular Visual Studio add-ons. 139 | 140 | # User-specific files 141 | *.userosscache 142 | *.sln.docstates 143 | 144 | # User-specific files (MonoDevelop/Xamarin Studio) 145 | *.userprefs 146 | 147 | # Build results 148 | [Dd]ebug/ 149 | [Dd]ebugPublic/ 150 | [Rr]elease/ 151 | [Rr]eleases/ 152 | x64/ 153 | x86/ 154 | bld/ 155 | build/ 156 | [Bb]in/ 157 | [Oo]bj/ 158 | [Ll]og/ 159 | 160 | # Visual Studio 2015 cache/options directory 161 | .vs/ 162 | # Uncomment if you have tasks that create the project's static files in wwwroot 163 | #wwwroot/ 164 | 165 | # MSTest test Results 166 | [Tt]est[Rr]esult*/ 167 | [Bb]uild[Ll]og.* 168 | 169 | # NUNIT 170 | *.VisualState.xml 171 | TestResult.xml 172 | 173 | # Build Results of an ATL Project 174 | [Dd]ebugPS/ 175 | [Rr]eleasePS/ 176 | dlldata.c 177 | 178 | # DNX 179 | project.lock.json 180 | project.fragment.lock.json 181 | artifacts/ 182 | 183 | *_i.c 184 | *_p.c 185 | *_i.h 186 | *.ilk 187 | *.meta 188 | *.obj 189 | *.pch 190 | *.pdb 191 | *.pgc 192 | *.pgd 193 | *.rsp 194 | *.sbr 195 | *.tlb 196 | *.tli 197 | *.tlh 198 | *.tmp 199 | *.tmp_proj 200 | *.log 201 | *.vspscc 202 | *.vssscc 203 | .builds 204 | *.pidb 205 | *.svclog 206 | *.scc 207 | 208 | # Chutzpah Test files 209 | _Chutzpah* 210 | 211 | # Visual C++ cache files 212 | ipch/ 213 | *.aps 214 | *.ncb 215 | *.opendb 216 | *.opensdf 217 | *.sdf 218 | *.cachefile 219 | *.VC.db 220 | *.VC.VC.opendb 221 | 222 | # Visual Studio profiler 223 | *.psess 224 | *.vsp 225 | *.vspx 226 | *.sap 227 | 228 | # TFS 2012 Local Workspace 229 | $tf/ 230 | 231 | # Guidance Automation Toolkit 232 | *.gpState 233 | 234 | # ReSharper is a .NET coding add-in 235 | _ReSharper*/ 236 | *.[Rr]e[Ss]harper 237 | *.DotSettings.user 238 | 239 | # JustCode is a .NET coding add-in 240 | .JustCode 241 | 242 | # TeamCity is a build add-in 243 | _TeamCity* 244 | 245 | # DotCover is a Code Coverage Tool 246 | *.dotCover 247 | 248 | # NCrunch 249 | _NCrunch_* 250 | .*crunch*.local.xml 251 | nCrunchTemp_* 252 | 253 | # MightyMoose 254 | *.mm.* 255 | AutoTest.Net/ 256 | 257 | # Web workbench (sass) 258 | .sass-cache/ 259 | 260 | # Installshield output folder 261 | [Ee]xpress/ 262 | 263 | # DocProject is a documentation generator add-in 264 | DocProject/buildhelp/ 265 | DocProject/Help/*.HxT 266 | DocProject/Help/*.HxC 267 | DocProject/Help/*.hhc 268 | DocProject/Help/*.hhk 269 | DocProject/Help/*.hhp 270 | DocProject/Help/Html2 271 | DocProject/Help/html 272 | 273 | # Click-Once directory 274 | publish/ 275 | 276 | # Publish Web Output 277 | *.[Pp]ublish.xml 278 | *.azurePubxml 279 | # TODO: Comment the next line if you want to checkin your web deploy settings 280 | # but database connection strings (with potential passwords) will be unencrypted 281 | *.pubxml 282 | *.publishproj 283 | 284 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 285 | # checkin your Azure Web App publish settings, but sensitive information contained 286 | # in these scripts will be unencrypted 287 | PublishScripts/ 288 | 289 | # NuGet Packages 290 | *.nupkg 291 | # The packages folder can be ignored because of Package Restore 292 | **/packages/* 293 | # except build/, which is used as an MSBuild target. 294 | !**/packages/build/ 295 | # Uncomment if necessary however generally it will be regenerated when needed 296 | #!**/packages/repositories.config 297 | # NuGet v3's project.json files produces more ignoreable files 298 | *.nuget.props 299 | *.nuget.targets 300 | 301 | # Microsoft Azure Build Output 302 | csx/ 303 | *.build.csdef 304 | 305 | # Microsoft Azure Emulator 306 | ecf/ 307 | rcf/ 308 | 309 | # Windows Store app package directories and files 310 | AppPackages/ 311 | BundleArtifacts/ 312 | Package.StoreAssociation.xml 313 | _pkginfo.txt 314 | 315 | # Visual Studio cache files 316 | # files ending in .cache can be ignored 317 | *.[Cc]ache 318 | # but keep track of directories ending in .cache 319 | !*.[Cc]ache/ 320 | 321 | # Others 322 | ClientBin/ 323 | ~$* 324 | *~ 325 | *.dbmdl 326 | *.dbproj.schemaview 327 | *.pfx 328 | *.publishsettings 329 | node_modules/ 330 | orleans.codegen.cs 331 | 332 | # Since there are multiple workflows, uncomment next line to ignore bower_components 333 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 334 | #bower_components/ 335 | 336 | # RIA/Silverlight projects 337 | Generated_Code/ 338 | 339 | # Backup & report files from converting an old project file 340 | # to a newer Visual Studio version. Backup files are not needed, 341 | # because we have git ;-) 342 | _UpgradeReport_Files/ 343 | Backup*/ 344 | UpgradeLog*.XML 345 | UpgradeLog*.htm 346 | 347 | # SQL Server files 348 | *.mdf 349 | *.ldf 350 | 351 | # Business Intelligence projects 352 | *.rdl.data 353 | *.bim.layout 354 | *.bim_*.settings 355 | 356 | # Microsoft Fakes 357 | FakesAssemblies/ 358 | 359 | # GhostDoc plugin setting file 360 | *.GhostDoc.xml 361 | 362 | # Node.js Tools for Visual Studio 363 | .ntvs_analysis.dat 364 | 365 | # Visual Studio 6 build log 366 | *.plg 367 | 368 | # Visual Studio 6 workspace options file 369 | *.opt 370 | 371 | # Visual Studio LightSwitch build output 372 | **/*.HTMLClient/GeneratedArtifacts 373 | **/*.DesktopClient/GeneratedArtifacts 374 | **/*.DesktopClient/ModelManifest.xml 375 | **/*.Server/GeneratedArtifacts 376 | **/*.Server/ModelManifest.xml 377 | _Pvt_Extensions 378 | 379 | # Paket dependency manager 380 | .paket/paket.exe 381 | paket-files/ 382 | 383 | # FAKE - F# Make 384 | .fake/ 385 | 386 | # JetBrains Rider 387 | .idea/ 388 | *.sln.iml 389 | -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | true 8 | $(MSBuildThisFileDirectory) 9 | $(MSBuildThisFileDirectory)..\ 10 | /Library/Frameworks/Mono.framework/Commands/mono 11 | mono 12 | 13 | 14 | 15 | $(PaketRootPath)paket.exe 16 | $(PaketToolsPath)paket.exe 17 | $(PaketToolsPath)paket.bootstrapper.exe 18 | "$(PaketExePath)" 19 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 20 | "$(PaketBootStrapperExePath)" $(PaketBootStrapperCommandArgs) 21 | $(MonoPath) --runtime=v4.0.30319 $(PaketBootStrapperExePath) $(PaketBootStrapperCommandArgs) 22 | 23 | $(MSBuildProjectDirectory)\paket.references 24 | $(MSBuildStartupDirectory)\paket.references 25 | $(MSBuildProjectFullPath).paket.references 26 | $(PaketCommand) restore --references-files "$(PaketReferences)" 27 | $(PaketBootStrapperCommand) 28 | 29 | RestorePackages; $(BuildDependsOn); 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F# Typeclasses: a Dictionary Encoding Approach 2 | 3 | A lot of people have tried to encode Haskell-style typeclasses or 4 | ML-style modules and functors in F#. So far there have been two major 5 | stumbling blocks: (1) F#'s lack of higher-kinded types, and (2) F#'s 6 | lack of a compile-time mechanism for implicitly selecting 7 | implementations based on types. 8 | 9 | The state of the art in F# seems to be 10 | https://github.com/palladin/Higher , a library for higher-kinded 11 | programming in F#. The problem with Higher and most other F# typeclass 12 | efforts is that they make heavy use of object-oriented techniques, 13 | which--although they work--are clunky and difficult to implement. 14 | Writing new instances--the very thing that gives typeclasses their 15 | extensibility--should be a piece of cake. 16 | 17 | This library, `fsharp-typeclasses`, aims to make that possible by 18 | borrowing ideas heavily from both Haskell and Scala. 19 | 20 | ## Approach 21 | 22 | The basic idea is to encode a typeclass as a record type holding the 23 | methods (the typeclass operations), and encode instances as record 24 | values. This is pretty much how Haskell implements it behind the scenes. 25 | However, we take some code organisation steps to make things easier on 26 | ourselves. 27 | 28 | ### Module Structure 29 | 30 | We structure a typeclass (e.g. `Functor`) as a `module Functor` in a 31 | file `functor.fs` with a corresponding signature file `functor.fsi`. We 32 | use the signature file heavily in our approach, to help us constrain our 33 | types but also keep the implementation clean and clutter-free. 34 | 35 | In the `Functor` module we have a record type `t` which has a field 36 | corresponding to the `Functor` map method, with an appropriate function 37 | type. 38 | 39 | Next, we have the various instances which are appropriate to declare in 40 | the typeclass module, as they're based on standard library types like 41 | `option`, `list` and `array`. 42 | 43 | Next, we have a nested module `Ops` which exposes the typeclass's 44 | methods in an easy-to-use way given any typeclass instance. The 45 | intention is that the user will open this module to get all the 46 | typeclass operations; they won't actually have to open the actual 47 | top-level typeclass module itself. Of course, they'll need to pass in 48 | instances, but they can access the instances safely qualified by the 49 | module name, e.g. 50 | 51 | ```fsharp 52 | open Functor.Ops 53 | 54 | let list = map Functor.list fst [1, 2; 3, 4] 55 | // list = [1; 3] 56 | ``` 57 | 58 | Finally, we (optionally) have another nested module `Laws` which encodes 59 | the laws the typeclass is expected to obey in the form of functions 60 | which take the relevant instances and any other inputs they need and 61 | output a `bool` indicating whether the law is obeyed or not for those 62 | instances and values. This practice is also used quite often in Haskell 63 | and Scala to ship typeclasses with their expected invariant behaviours 64 | in the code itself. 65 | 66 | ### Signature Files 67 | 68 | As mentioned, we make heavy use of signature files to assign exact 69 | typing information to all our functions and values. The feedback loop 70 | between implementation and signature files helps to derive 71 | higher-quality code. 72 | 73 | ### Higher-Kinded Types 74 | 75 | We skip over the problem of the lack of higher-kinded types by just 76 | passing in _fully-applied_ types as type parameters. This means that 77 | instead of having a `Functor.t<'f>`, we have a `Functor.t<'a, 'b, 'fa, 78 | 'fb>`. Now, admittedly, there's no guarantee with these type parameters 79 | that they'll actually obey the functor requirements, e.g. if `'fa` is a 80 | `list<'a>` then `'fb` must be a `list<'b>`. There's nothing enforcing 81 | this at compile time. But in my opinion we partially make up for that by 82 | shipping functor laws right beside the functor definition; once we 83 | implement property-based testing of the laws, they will be almost as 84 | solid as in languages with higher-kinded types. 85 | 86 | ## Explicit Dictionary Passing 87 | 88 | For now, we are forced to pass in typeclass instances explicitly as 89 | shown above. F# does not have a general-purpose implicit resolution 90 | technique available. However, we are no worse off here than with ML 91 | modules and having work with them explicitly. Also, there may be some 92 | way to leverage F# code quotations to simulate implicit resolution. 93 | 94 | ## Exploration with JSON Encoding 95 | 96 | So far the most comprehensive exploration of this typeclass technique is 97 | in the `To_json` module. We provide instances for simple types and 98 | combinators to derive instances, ranging in complexity from simple 99 | (deriving a JSON converter of an array of something given a converter 100 | instance for that thing) to complex (deriving a converter instances for 101 | an arbitrary product type given multiple converter instances for its 102 | component types). 103 | 104 | Note in particular the full suite of unit tests of the JSON instances in 105 | the `FsharpTypeclassesTest.To_json_test` module. These tests show a good 106 | cross-section of usages, from building instances out of simpler 107 | instances (much like ML functors, actually) to using the instances for 108 | JSON conversion. 109 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | .paket\paket.bootstrapper.exe 5 | if errorlevel 1 ( 6 | exit /b %errorlevel% 7 | ) 8 | 9 | .paket\paket.exe restore 10 | if errorlevel 1 ( 11 | exit /b %errorlevel% 12 | ) 13 | 14 | packages\FAKE\tools\FAKE.exe build.fsx %* 15 | -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | // include Fake libs 2 | #r "./packages/FAKE/tools/FakeLib.dll" 3 | 4 | open Fake 5 | open Fake.Testing 6 | 7 | // Directories 8 | let buildDir = "./build/" 9 | let deployDir = "./deploy/" 10 | 11 | // Filesets 12 | let appReferences = 13 | !! "/**/*.csproj" 14 | ++ "/**/*.fsproj" 15 | 16 | // version info 17 | let version = "0.1" // or retrieve from CI server 18 | 19 | // Targets 20 | Target ? Clean <- fun _ -> CleanDirs [buildDir; deployDir] 21 | Target ? Build <- fun _ -> 22 | MSBuildDebug buildDir "Build" appReferences |> Log "AppBuild-Output: " 23 | 24 | Target ? BuildTest <- fun _ -> 25 | !! ("src/FSharpTypeclasses/**/*.fsproj") 26 | |> MSBuildDebug buildDir "Build" 27 | |> Log "TestBuild-Output: " 28 | 29 | Target ? Test <- fun _ -> 30 | !! (buildDir + "FSharpTypeclassesTest.dll") 31 | |> NUnit3 (fun p -> 32 | { p with 33 | ToolPath = 34 | "packages/NUnit.ConsoleRunner/tools/nunit3-console.exe" }) 35 | 36 | Target ? Deploy <- fun _ -> 37 | !! (buildDir + "/**/*.*") 38 | -- "*.zip" 39 | |> Zip buildDir (deployDir + "ApplicationName." + version + ".zip") 40 | 41 | // Build order 42 | "Clean" 43 | ==> "Build" 44 | ==> "BuildTest" 45 | ==> "Test" 46 | ==> "Deploy" 47 | 48 | // start build 49 | RunTargetOrDefault ? Build 50 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if test "$OS" = "Windows_NT" 3 | then 4 | # use .Net 5 | 6 | .paket/paket.bootstrapper.exe 7 | exit_code=$? 8 | if [ $exit_code -ne 0 ]; then 9 | exit $exit_code 10 | fi 11 | 12 | .paket/paket.exe restore 13 | exit_code=$? 14 | if [ $exit_code -ne 0 ]; then 15 | exit $exit_code 16 | fi 17 | 18 | packages/FAKE/tools/FAKE.exe $@ --fsiargs build.fsx 19 | else 20 | # use mono 21 | mono .paket/paket.bootstrapper.exe 22 | exit_code=$? 23 | if [ $exit_code -ne 0 ]; then 24 | exit $exit_code 25 | fi 26 | 27 | mono .paket/paket.exe restore 28 | exit_code=$? 29 | if [ $exit_code -ne 0 ]; then 30 | exit $exit_code 31 | fi 32 | mono packages/FAKE/tools/FAKE.exe $@ --fsiargs -d:MONO build.fsx 33 | fi 34 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://www.nuget.org/api/v2 2 | nuget FAKE 3 | nuget FSharp.Core 4 | nuget NUnit 5 | nuget NUnit.ConsoleRunner 6 | -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | NUGET 2 | remote: https://www.nuget.org/api/v2 3 | FAKE (4.42) 4 | FSharp.Core (4.0.0.1) 5 | NUnit (3.5) 6 | NUnit.ConsoleRunner (3.5) 7 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/FSharpTypeclasses.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | FSharpTypeclasses 5 | FSharpTypeclasses 6 | FSharpTypeclasses 7 | Debug 8 | AnyCPU 9 | 2.0 10 | 4365901e-7dcc-480e-878d-fe6a087807a7 11 | Library 12 | v4.5 13 | true 14 | 4.4.0.0 15 | 16 | 17 | true 18 | Full 19 | false 20 | false 21 | bin\$(Configuration)\ 22 | DEBUG;TRACE 23 | 3 24 | $(Platform) 25 | 26 | 27 | PdbOnly 28 | true 29 | true 30 | bin\$(Configuration)\ 31 | TRACE 32 | 3 33 | $(Platform) 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 55 | 56 | 57 | 58 | 59 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\4.0\Framework\v4.0\Microsoft.FSharp.Targets 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll 69 | True 70 | True 71 | 72 | 73 | 74 | 75 | 76 | 77 | ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll 78 | True 79 | True 80 | 81 | 82 | 83 | 84 | 85 | 86 | ..\..\packages\FSharp.Core\lib\portable-net45+monoandroid10+monotouch10+xamarinios10\FSharp.Core.dll 87 | True 88 | True 89 | 90 | 91 | 92 | 93 | 94 | 95 | ..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll 96 | True 97 | True 98 | 99 | 100 | 101 | 102 | 103 | 104 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll 105 | True 106 | True 107 | 108 | 109 | 110 | 111 | 112 | 113 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll 114 | True 115 | True 116 | 117 | 118 | 119 | 120 | 121 | 122 | ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll 123 | True 124 | True 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/eq.fs: -------------------------------------------------------------------------------- 1 | namespace FsharpTypeclasses 2 | 3 | [] 4 | module Eq = 5 | type t<'a> = { eq : 'a -> 'a -> bool } 6 | 7 | let equality = { eq = (=) } 8 | 9 | module Ops = let eq t = t.eq 10 | module Laws = let reflexivity t a = Ops.eq t a a 11 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/eq.fsi: -------------------------------------------------------------------------------- 1 | namespace FsharpTypeclasses 2 | 3 | module Eq = 4 | type t<'a> 5 | 6 | val equality : t<'a> when 'a : equality 7 | 8 | module Ops = val eq : t<'a> -> ('a -> 'a -> bool) 9 | module Laws = val reflexivity : t<'a> -> 'a -> bool 10 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/functor.fs: -------------------------------------------------------------------------------- 1 | namespace FsharpTypeclasses 2 | 3 | [] 4 | module Functor = 5 | type t<'a, 'b, 'fa, 'fb> = { map : ('a -> 'b) -> 'fa -> 'fb } 6 | 7 | let option = { map = Option.map } 8 | let list = { map = List.map } 9 | let array = { map = Array.map } 10 | let tuple2 = { map = fun f (a1, a2) -> (f a1, f a2) } 11 | 12 | module Ops = let map t = t.map 13 | module Laws = 14 | open Eq.Ops 15 | open Ops 16 | 17 | let identity t eq_t fa = eq eq_t fa <| map t id fa 18 | let composition t1 t2 t3 eq_t f g fa = 19 | let lhs = map t2 g <| map t1 f fa 20 | let rhs = map t3 (g << f) fa 21 | 22 | eq eq_t lhs rhs 23 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/functor.fsi: -------------------------------------------------------------------------------- 1 | namespace FsharpTypeclasses 2 | 3 | module Functor = 4 | type t<'a, 'b, 'fa, 'fb> 5 | 6 | val option : t<'a, 'b, option<'a>, option<'b>> 7 | val list : t<'a, 'b, list<'a>, list<'b>> 8 | val array : t<'a, 'b, array<'a>, array<'b>> 9 | val tuple2 : t<'a, 'b, 'a * 'a, 'b * 'b> 10 | 11 | module Ops = 12 | val map : t<'a, 'b, 'fa, 'fb> -> (('a -> 'b) -> 'fa -> 'fb) 13 | 14 | module Laws = 15 | val identity : t<'a, 'a, 'fa, 'fa> -> Eq.t<'fa> -> 'fa -> bool 16 | val composition : 17 | t<'a, 'b, 'fa, 'fb> -> 18 | t<'b, 'c, 'fb, 'fc> -> 19 | t<'a, 'c, 'fa, 'fc> -> 20 | Eq.t<'fc> -> 21 | ('a -> 'b) -> 22 | ('b -> 'c) -> 23 | 'fa -> 24 | bool 25 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/to_json.fs: -------------------------------------------------------------------------------- 1 | namespace FsharpTypeclasses 2 | 3 | open System.Collections.Generic 4 | 5 | [] 6 | module To_json = 7 | type t<'a> = { apply : 'a -> string } 8 | type key_value<'a> = KeyValuePair 9 | 10 | let commalist = String.concat "," 11 | let enbrace s = "{" + s + "}" 12 | 13 | let int = { apply = sprintf "%d" } 14 | let string = { apply = sprintf "\"%s\"" } 15 | let float : t = { apply = sprintf "%A" } 16 | let double : t = { apply = sprintf "%A" } 17 | let char = { apply = sprintf "\"%c\"" } 18 | let bool = { apply = sprintf "%b" } 19 | let option t = { apply = Option.fold (fun _ -> t.apply) "null" } 20 | let array t = 21 | { apply = fun a -> 22 | "[" + (a |> Array.map t.apply |> commalist) + "]" } 23 | 24 | (** 25 | Returns a To_json instance that can convert a single key-value pair 26 | into a JSON string. Note that by itself this conversion isn't a 27 | well-formed JSON string; you'll need to encapsulate it in one of the 28 | `objectX` instances. 29 | 30 | @param t a JSON converter instance for the value in the key-value 31 | pair. 32 | *) 33 | let key_value t : t> = 34 | { apply = fun a -> 35 | (string.apply a.Key) + ":" + (t.apply a.Value) } 36 | 37 | let object1 (f1, t1) = 38 | { apply = enbrace << (key_value t1).apply << f1 } 39 | 40 | let object2 (f1, t1) (f2, t2) = 41 | { apply = fun a -> 42 | [| a |> f1 |> (key_value t1).apply 43 | a |> f2 |> (key_value t2).apply |] 44 | |> commalist |> enbrace } 45 | 46 | let object3 (f1, t1) (f2, t2) (f3, t3) = 47 | { apply = fun a -> 48 | [| a |> f1 |> (key_value t1).apply 49 | a |> f2 |> (key_value t2).apply 50 | a |> f3 |> (key_value t3).apply |] 51 | |> commalist |> enbrace } 52 | 53 | let object4 (f1, t1) (f2, t2) (f3, t3) (f4, t4) = 54 | { apply = fun a -> 55 | [| a |> f1 |> (key_value t1).apply 56 | a |> f2 |> (key_value t2).apply 57 | a |> f3 |> (key_value t3).apply 58 | a |> f4 |> (key_value t4).apply |] 59 | |> commalist |> enbrace } 60 | 61 | let object5 (f1, t1) (f2, t2) (f3, t3) (f4, t4) (f5, t5) = 62 | { apply = fun a -> 63 | [| a |> f1 |> (key_value t1).apply 64 | a |> f2 |> (key_value t2).apply 65 | a |> f3 |> (key_value t3).apply 66 | a |> f4 |> (key_value t4).apply 67 | a |> f5 |> (key_value t5).apply |] 68 | |> commalist |> enbrace } 69 | 70 | let object6 (f1, t1) (f2, t2) (f3, t3) (f4, t4) (f5, t5) (f6, t6) = 71 | { apply = fun a -> 72 | [| a |> f1 |> (key_value t1).apply 73 | a |> f2 |> (key_value t2).apply 74 | a |> f3 |> (key_value t3).apply 75 | a |> f4 |> (key_value t4).apply 76 | a |> f5 |> (key_value t5).apply 77 | a |> f6 |> (key_value t6).apply |] 78 | |> commalist |> enbrace } 79 | 80 | let object7 81 | (f1, t1) (f2, t2) (f3, t3) (f4, t4) (f5, t5) (f6, t6) (f7, t7) = 82 | { apply = fun a -> 83 | [| a |> f1 |> (key_value t1).apply 84 | a |> f2 |> (key_value t2).apply 85 | a |> f3 |> (key_value t3).apply 86 | a |> f4 |> (key_value t4).apply 87 | a |> f5 |> (key_value t5).apply 88 | a |> f6 |> (key_value t6).apply 89 | a |> f7 |> (key_value t7).apply |] 90 | |> commalist |> enbrace } 91 | 92 | let object8 93 | (f1, t1) 94 | (f2, t2) 95 | (f3, t3) 96 | (f4, t4) 97 | (f5, t5) 98 | (f6, t6) 99 | (f7, t7) 100 | (f8, t8) = 101 | { apply = fun a -> 102 | [| a |> f1 |> (key_value t1).apply 103 | a |> f2 |> (key_value t2).apply 104 | a |> f3 |> (key_value t3).apply 105 | a |> f4 |> (key_value t4).apply 106 | a |> f5 |> (key_value t5).apply 107 | a |> f6 |> (key_value t6).apply 108 | a |> f7 |> (key_value t7).apply 109 | a |> f8 |> (key_value t8).apply |] 110 | |> commalist |> enbrace } 111 | 112 | module Ops = 113 | let key (name : string) value = KeyValuePair(name, value) 114 | let to_json t = t.apply 115 | let make_to_json f = { apply = f } 116 | -------------------------------------------------------------------------------- /src/FSharpTypeclasses/to_json.fsi: -------------------------------------------------------------------------------- 1 | namespace FsharpTypeclasses 2 | 3 | open System.Collections.Generic 4 | 5 | (** 6 | The To_json.t<'a> typeclass, instances for primitive types, and instance 7 | builders for arbitrary (product) data types. 8 | *) 9 | module To_json = 10 | (** 11 | Essentially this is just a wrapper for a function which can convert a 12 | given type 'a to a JSON string. A good way to think of it is also as a 13 | proposition that a type 'a can be converted into a JSON string. The 14 | typeclass instances of the various types are proofs that those types 15 | can be converted into JSON strings. 16 | *) 17 | type t<'a> 18 | type key_value<'a> = KeyValuePair 19 | 20 | (* JSON converter instances for primitive types. *) 21 | 22 | val int : t 23 | val string : t 24 | val float : t 25 | val double : t 26 | val char : t 27 | val bool : t 28 | val option : t<'a> -> t> 29 | val array : t<'a> -> t> 30 | 31 | (* 32 | The following are used to create JSON converter instances that 33 | construct valid JSON objects out of at least one key-value pair. They 34 | work by extracting (or converting) the input value (of type 'a) into 35 | one or more output values which we already know can be converted. This 36 | way we can convert recursively into an arbitrary depth. 37 | *) 38 | 39 | val object1 : ('a -> key_value<'b1>) * t<'b1> -> t<'a> 40 | val object2 : 41 | ('a -> key_value<'b1>) * t<'b1> -> 42 | ('a -> key_value<'b2>) * t<'b2> -> 43 | t<'a> 44 | 45 | val object3 : 46 | ('a -> key_value<'b1>) * t<'b1> -> 47 | ('a -> key_value<'b2>) * t<'b2> -> 48 | ('a -> key_value<'b3>) * t<'b3> -> 49 | t<'a> 50 | 51 | val object4 : 52 | ('a -> key_value<'b1>) * t<'b1> -> 53 | ('a -> key_value<'b2>) * t<'b2> -> 54 | ('a -> key_value<'b3>) * t<'b3> -> 55 | ('a -> key_value<'b4>) * t<'b4> -> 56 | t<'a> 57 | 58 | val object5 : 59 | ('a -> key_value<'b1>) * t<'b1> -> 60 | ('a -> key_value<'b2>) * t<'b2> -> 61 | ('a -> key_value<'b3>) * t<'b3> -> 62 | ('a -> key_value<'b4>) * t<'b4> -> 63 | ('a -> key_value<'b5>) * t<'b5> -> 64 | t<'a> 65 | 66 | val object6 : 67 | ('a -> key_value<'b1>) * t<'b1> -> 68 | ('a -> key_value<'b2>) * t<'b2> -> 69 | ('a -> key_value<'b3>) * t<'b3> -> 70 | ('a -> key_value<'b4>) * t<'b4> -> 71 | ('a -> key_value<'b5>) * t<'b5> -> 72 | ('a -> key_value<'b6>) * t<'b6> -> 73 | t<'a> 74 | 75 | val object7 : 76 | ('a -> key_value<'b1>) * t<'b1> -> 77 | ('a -> key_value<'b2>) * t<'b2> -> 78 | ('a -> key_value<'b3>) * t<'b3> -> 79 | ('a -> key_value<'b4>) * t<'b4> -> 80 | ('a -> key_value<'b5>) * t<'b5> -> 81 | ('a -> key_value<'b6>) * t<'b6> -> 82 | ('a -> key_value<'b7>) * t<'b7> -> 83 | t<'a> 84 | 85 | val object8 : 86 | ('a -> key_value<'b1>) * t<'b1> -> 87 | ('a -> key_value<'b2>) * t<'b2> -> 88 | ('a -> key_value<'b3>) * t<'b3> -> 89 | ('a -> key_value<'b4>) * t<'b4> -> 90 | ('a -> key_value<'b5>) * t<'b5> -> 91 | ('a -> key_value<'b6>) * t<'b6> -> 92 | ('a -> key_value<'b7>) * t<'b7> -> 93 | ('a -> key_value<'b8>) * t<'b8> -> 94 | t<'a> 95 | 96 | (** Operations meant to be imported directly into client code. *) 97 | module Ops = 98 | (** 99 | Returns a key-value pair of the given name and value. This is a 100 | helper function to make creating JSON instances easier. 101 | *) 102 | val key : name:string -> value:'a -> key_value<'a> 103 | 104 | (** 105 | Returns a JSON converter function from some type 'a to a string. 106 | *) 107 | val to_json : t<'a> -> ('a -> string) 108 | 109 | (** 110 | Returns a JSON converter instance for an arbitrary type 'a given a 111 | function that can convert an 'a value into a JSON string. 112 | *) 113 | val make_to_json : ('a -> string) -> t<'a> 114 | -------------------------------------------------------------------------------- /src/FSharpTypeclassesTest/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/FSharpTypeclassesTest/FSharpTypeclassesTest.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.0 8 | c533e41f-0db6-4de0-8cfa-c6223ea795f2 9 | Library 10 | FSharpTypeclassesTest 11 | FSharpTypeclassesTest 12 | v4.5 13 | true 14 | 4.4.0.0 15 | FSharpTypeclassesTest 16 | 17 | 18 | true 19 | full 20 | false 21 | false 22 | bin\$(Configuration)\ 23 | DEBUG;TRACE 24 | 3 25 | $(Platform) 26 | bin\$(Configuration)\$(AssemblyName).XML 27 | 28 | 29 | pdbonly 30 | true 31 | true 32 | bin\$(Configuration)\ 33 | TRACE 34 | 3 35 | $(Platform) 36 | bin\$(Configuration)\$(AssemblyName).XML 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | FSharpTypeclasses 47 | {4365901e-7dcc-480e-878d-fe6a087807a7} 48 | True 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 14 57 | 58 | 59 | 60 | 61 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets 62 | 63 | 64 | 65 | 66 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\4.0\Framework\v4.0\Microsoft.FSharp.Targets 67 | 68 | 69 | 70 | 71 | 78 | 79 | 80 | 81 | 82 | ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll 83 | True 84 | True 85 | 86 | 87 | 88 | 89 | 90 | 91 | ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll 92 | True 93 | True 94 | 95 | 96 | 97 | 98 | 99 | 100 | ..\..\packages\FSharp.Core\lib\portable-net45+monoandroid10+monotouch10+xamarinios10\FSharp.Core.dll 101 | True 102 | True 103 | 104 | 105 | 106 | 107 | 108 | 109 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll 110 | True 111 | True 112 | 113 | 114 | 115 | 116 | 117 | 118 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll 119 | True 120 | True 121 | 122 | 123 | 124 | 125 | 126 | 127 | ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll 128 | True 129 | True 130 | 131 | 132 | 133 | 134 | 135 | 136 | ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll 137 | True 138 | True 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | ..\..\packages\NUnit\lib\net20\NUnit.System.Linq.dll 148 | True 149 | True 150 | 151 | 152 | ..\..\packages\NUnit\lib\net20\nunit.framework.dll 153 | True 154 | True 155 | 156 | 157 | 158 | 159 | 160 | 161 | ..\..\packages\NUnit\lib\net35\nunit.framework.dll 162 | True 163 | True 164 | 165 | 166 | 167 | 168 | 169 | 170 | ..\..\packages\NUnit\lib\net40\nunit.framework.dll 171 | True 172 | True 173 | 174 | 175 | 176 | 177 | 178 | 179 | ..\..\packages\NUnit\lib\net45\nunit.framework.dll 180 | True 181 | True 182 | 183 | 184 | 185 | 186 | 187 | 188 | ..\..\packages\NUnit\lib\portable-net45+win8+wp8+wpa81+Xamarin.Mac+MonoAndroid10+MonoTouch10+Xamarin.iOS10\nunit.framework.dll 189 | True 190 | True 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /src/FSharpTypeclassesTest/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | NUnit 3 | -------------------------------------------------------------------------------- /src/FSharpTypeclassesTest/to_json_test.fs: -------------------------------------------------------------------------------- 1 | namespace FsharpTypeclassesTest 2 | 3 | open FsharpTypeclasses 4 | open NUnit.Framework 5 | open To_json.Ops 6 | 7 | [] 8 | module To_json_test = 9 | let option_int = To_json.option To_json.int 10 | 11 | [] 12 | let int () = 13 | Assert.AreEqual("1", to_json To_json.int 1) 14 | 15 | [] 16 | let string () = 17 | Assert.AreEqual("\"1\"", to_json To_json.string "1") 18 | 19 | [] 20 | let float () = 21 | Assert.AreEqual("1.0", to_json To_json.float 1.0) 22 | 23 | [] 24 | let double () = 25 | Assert.AreEqual("1.0", to_json To_json.double 1.0) 26 | 27 | [] 28 | let char () = 29 | Assert.AreEqual("\"1\"", to_json To_json.char '1') 30 | 31 | [] 32 | let bool () = 33 | Assert.AreEqual("false", to_json To_json.bool false) 34 | Assert.AreEqual("true", to_json To_json.bool true) 35 | 36 | [] 37 | let option () = 38 | Assert.AreEqual("null", to_json option_int None) 39 | Assert.AreEqual("1", to_json option_int <| Some 1) 40 | 41 | [] 42 | let array () = 43 | let array_int = To_json.array To_json.int 44 | 45 | Assert.AreEqual("[]", to_json array_int Array.empty) 46 | Assert.AreEqual("[1]", to_json array_int [| 1 |]) 47 | Assert.AreEqual("[1,2]", to_json array_int [| 1; 2 |]) 48 | 49 | type person = { name : string; age : option } 50 | 51 | let name person = person.name 52 | let age person = person.age 53 | 54 | (** A person to JSON converter. *) 55 | let to_json_person = 56 | To_json.object2 57 | (key "name" << name, To_json.string) 58 | (key "age" << age, option_int) 59 | 60 | [] 61 | let object2 () = 62 | let person = { name = "bob"; age = Some 31 } 63 | let json = """{"name":"bob","age":31}""" 64 | 65 | Assert.AreEqual(json, to_json to_json_person person) 66 | --------------------------------------------------------------------------------