diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0f5c212f..36068350 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -41,7 +41,7 @@ jobs: fi shell: bash - name: Install R package dependencies - run: Rscript -e "install.packages(c('tseries', 'zoo'), repos = 'https://cran.r-project.org')" + run: Rscript -e "install.packages(c('tseries', 'zoo', 'ggplot2', 'svglite'), repos = 'https://cran.r-project.org')" shell: bash - name: Setup .NET uses: actions/setup-dotnet@v1 diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index a821abd9..45fccb19 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -38,7 +38,7 @@ jobs: fi shell: bash - name: Install R package dependencies - run: Rscript -e "install.packages(c('tseries', 'zoo'), repos = 'https://cran.r-project.org')" + run: Rscript -e "install.packages(c('tseries', 'zoo', 'ggplot2', , 'svglite'), repos = 'https://cran.r-project.org')" shell: bash - name: Setup .NET uses: actions/setup-dotnet@v1 @@ -53,6 +53,7 @@ jobs: - name: Build run: dotnet fsi build.fsx - name: Deploy documentation from master + if: matrix.os == 'macos-latest' uses: peaceiris/actions-gh-pages@v3 with: personal_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/LICENSE.md b/LICENSE.md index 54d99deb..118a1de9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,8 +1,8 @@ RProvider License ================= -Copyright (c) 2012–2026, RProvider Contributors Copyright (c) 2012, BlueMountain Capital Management LLC +Copyright (c) 2021 - 2026, Andrew C. Martin All rights reserved. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 77af8313..ccb59195 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,4 @@ +* 3.0.0 - semantic types, RBridge, graphics module (svg outputs), new fsdocs template. * 3.0.0-alpha1 - replaced R.NET with RBridge, and updated to latest type provider SDK * 2.1.0 - new feature: ? and => operators; and locale fix for regions with comma number seperator * 2.0.3 - Include built-in FSI printer module diff --git a/build.fsx b/build.fsx index 3158da16..5df340a2 100644 --- a/build.fsx +++ b/build.fsx @@ -32,7 +32,7 @@ let projectSummary = let projectDescription = """ - An F# Type Provider providing strongly typed access to the R statistical package. + An F# Type Provider providing strongly typed access to the R statistical language. The type provider automatically discovers available R packages and makes them easily accessible from F#, so you can easily call powerful packages and visualization libraries from code running on the .NET platform.""" @@ -241,7 +241,7 @@ Target.create sprintf "%s/master/docs/content/logo.png" repositoryContentUrl sprintf "%s" tags sprintf "%s" release.NugetVersion - sprintf "%s/master/docs/img/logo.png" repositoryContentUrl + sprintf "img/logo.png" sprintf "%s/blob/master/LICENSE.md" repositoryUrl sprintf "%s/blob/master/RELEASE_NOTES.md" repositoryUrl "true" @@ -258,7 +258,7 @@ Target.create DotNet.exec id "fsdocs" - ("build --clean --properties Configuration=Release --parameters fsdocs-package-version " + ("build --clean --eval --properties Configuration=Release --parameters fsdocs-package-version " + release.NugetVersion) |> ignore) diff --git a/docs/expressions.fsx b/docs/expressions.fsx index dd19bf2b..4801528d 100644 --- a/docs/expressions.fsx +++ b/docs/expressions.fsx @@ -29,26 +29,146 @@ RProvider represents R objects and values through an `RExpr` type. RProvider inc open RProvider open RProvider.Operators -open RProvider.``base`` open RProvider.datasets open RProvider.stats (** +Next, let's use a sample R dataframe to demonstrate core R expression functions. +*) + +let df = R.penguins + +(** +Note: in most cases, there are equivalent functions available using either the module (`RExpr.*`) or using dot notation as we preference below. + +### Inpecting a value + +You may print the expression identically to using R print() using the .Print function or `RExpr.printToString`. +*) + +df.Print() + +(** +When using F# interactive (fsi), you may register a pretty-printer to print R values automatically into the terminal using `fsi.AddPrinter FSIPrinters.rValue`. + +We can also inspect the 'semantic type' - the standard R shape - of the R expression. +The possible shapes are listed in the `Runtime.RTypes.RSemanticType` discriminated union. +*) + +df.Type +(*** include-it ***) + +(** +Similarly, you may retrieve the classes of an R object using the classes property. +*) + +df.Class +(*** include-it ***) + +(** +#### Printing R values to the console (F# interactive) + +Add this line to your script to tell F# interactive how to print out +the values of R objects: + + [lang=fsharp] + fsi.AddPrinter FSIPrinters.rValue + + +### Extracting values (R -> F#) + +We can extract a value from R memory space into to F# primitive values in two ways: type-specific +or default type. For more information, see [passing data](passing-data.html). + +For example, let's extract the bill length column from the penguin dataset using the `RExpr` directly (without using the semantic types layer). To access the column, we can use the `?` operator; see [operators](operators.html) for details. +*) + +let billLength = df?bill_len +let billVal = df?bill_len |> RExpr.getValue + +(** +#### Extracting using the semantic R types layer +It is also possible to extract values through semantic types. Each semantic type has its own extraction functions specific to the type. For more information, see [semantic R types](semantic-types.html). + +Here, we can show how to extract a factor column using a type-safe pattern matching approach. +*) + +let speciesFactorSafe = + match df?species.TryAsTyped with + | Some t -> + match t with + | Runtime.RTypes.FactorInR f -> f.AsStringVector.Value + | _ -> [||] + | _ -> [||] + +(** +We can also do the same thing without try methods if we are certain of the type: +*) + +let speciesFactor = df?species.AsFactor().AsStringVector.Value +speciesFactor +(*** include-it ***) + +(** +*Note: Older RProvider versions contained a plugin system to register custom converters between .NET types and R types. This has been removed. The new preferred approach is to convert explicitly from semantic type wrappers. For example, an F# data frame library like Deedle could implement a custom conversion function from RProvider's `DataFrame` type.* + +### Viewing as an R semantic type + +A set of functions enable viewing an `RExpr` as RProvider's R semantic type wrappers. + +### Quick access to common properties + +The `RExpr` type has members for quick access into common required fields or properties of R objects. These are mirrored in the `RExpr`, which are easier to call with forward pipes for example. + +#### Vectors + +You can quickly access a single value within a vector using `.ValueAt` and the index. +*) + +let v = [ 0.1 .. 3.5 ] |> R.c + +let y : Runtime.RTypes.RScalar<1> = v.ValueAt 0 +y.AsReal().FromR.Value + +let z : Runtime.RTypes.RScalar<1> = v |> RExpr.typedVectorByIndex 0 +z.AsReal().FromR.Value + +(** +#### Lists and list-based objects + +S3 objects in R are basic R objects, but with a class attribute attached. +More often than not, they are lists. -# S4 classes +There are multiple quick-access methods to list items: + +* Using the ? operator from `RProvider.Operators`. +* Using the `RExpr.listItem` function. +*) + +let x = 10. +let summary = R.binom_test(x, 100., 0.5) // alternative = true + +summary?``p.value``.FromR() +summary?statistic.FromR() + +RExpr.listItem "p.value" summary + +(** +#### S4 objects (slots) For this example, let's set up an S4 class and object from scratch: *) -R.eval "setClass('testclass', representation(foo='character', bar='integer'))" -let s4 = R.eval "new('testclass', foo='s4', bar=1:4)" +R.parse(text = "setClass('testclass', representation(foo='character', bar='integer'))") |> R.eval +let s4 = R.parse(text = "new('testclass', foo='s4', bar=1:4)") |> R.eval +s4.Print() +(*** include-it ***) (** You can find out if there are slots using the `slots` and `trySlots` functions: *) s4 |> RExpr.slots -s4 |> RExpr.trySlots R.mtcars |> RExpr.trySlots (** @@ -58,3 +178,25 @@ You can access slot values similarly with the `slot` and `trySlot` functions: s4 |> RExpr.slot "foo" s4 |> RExpr.trySlot "foo" s4 |> RExpr.trySlot "doesntexist" + +(** +## Parallel processing + +R itself is not thread-safe. Most R parallel processing libraries focus on running multiple R processes and coordinating work and values between them. + +When using RProvider, you may use R from one or many threads. However, the underlying R engine being used is a single R instance. Internally, *RBridge* uses a concurrent queue to process incoming work. You will only gain the perception of multi-threading but none of the speed advantage when consuming R functions. +*) + +[| 1 .. 10 |] +|> Array.Parallel.map(fun i -> (R.sqrt i).AsScalar().AsReal().FromR.Value) +(*** include-it ***) + +[| 1 .. 10 |] +|> Array.map(fun i -> (R.sqrt i).AsScalar().AsReal().FromR.Value) +(*** include-it ***) + +(** +As can be seen, the results are identical. + +**Important**. Although *pure* functions return identical results, impure functions will not. The internal R instance is sharing it's environment, so you may cross contaminate. +*) \ No newline at end of file diff --git a/docs/img/bmc.png b/docs/img/bmc.png deleted file mode 100644 index 751e8da8..00000000 Binary files a/docs/img/bmc.png and /dev/null differ diff --git a/docs/img/vscode.gif b/docs/img/vscode.gif deleted file mode 100644 index c64f3599..00000000 Binary files a/docs/img/vscode.gif and /dev/null differ diff --git a/docs/index.fsx b/docs/index.fsx new file mode 100644 index 00000000..5e8f4e11 --- /dev/null +++ b/docs/index.fsx @@ -0,0 +1,86 @@ +(*** condition: prepare ***) +#nowarn "211" +#r "nuget: RProvider, 0.0.1-local" +(*** condition: fsx ***) +#if FSX +#r "nuget: RProvider,{{package-version}}" +#endif // FSX +(*** condition: ipynb ***) +#if IPYNB +#r "nuget: RProvider,{{package-version}}" +#endif // IPYNB + +(** +F# R Type Provider +======= + +**A typed interop layer that embeds R within F#.** + +The F# Type Provider enables interoperability between F# and [R](http://www.r-project.org/) through strongly-typed representations of F# types. The Type Provider discovers R packages that are available in an R installation and makes them available as namespaces in F#. + +From F#, you can call any R function, run statistical analyses, generate plots, and work with R objects interactively. This lets you combine R’s extensive statistical and visualisation ecosystem with F#'s succinct and expressive type system — including type providers, units of measure, and functional data pipelines. + +The below example shows a simple base R example within F#: +*) + +open RProvider +open RProvider.stats + +let x = [ 1. .. 5. ] |> R.c +let y = [ 1.2; 2.1; 2.9; 4.5; 4.8 ] |> R.c + +// Run a correlation test in R +let result = R.cor_test(x, y) + +// Extract results into F# +let pValue = result |> RExpr.listItem "p.value" |> RExpr.getValue +let statistic = result |> RExpr.listItem "statistic" |> RExpr.getValue +let estimate = result |> RExpr.listItem "estimate" |> RExpr.getValue + +printfn "Correlation estimate: %g\nTest statistic: %g\np-value: %g" estimate statistic pValue +(*** include-output ***) + +(** +The above example is run through F# interactive (`dotnet fsi`). + +## Using the R Type Provider + +**Prerequisites**. R 4.5.0 or higher, .NET 10 or higher, and the R_HOME environment variable set. [More info](requirements.html). + +In an F# script: +```fsharp +#r "nuget:RProvider" + +open RProvider +``` + +To add to a .NET project, from the terminal: +``` +dotnet add package RProvider +``` + +## What are R and F#? + +[F#](http://fsharp.org) is a multi-paradigm language +that supports functional, object and imperative programming, +with the emphasis on functional-first programming. F# runs on the .NET runtime and is a compiled, +statically typed language with a strong type system and type inference. +F# is a general purpose programming language, and is particularly well-suited for scientific/numerical computing. + +[R](http://www.r-project.org/) is a domain-specific language for statistical computing. +R has a rich ecosystem of community-developed packages across scientific disciplines. +R has many packages for publication-quality graphics, such as ggplot. +R is an interpreted, dynamically typed language for data exploration that is typically used +R-specific IDEs like [RStudio](http://www.rstudio.com/). + + +## Contributing and copyright + +The project was originally developed by [BlueMountain Capital](https://www.bluemountaincapital.com/), and has since been developed and open-source contributors. + +The project is hosted on [GitHub][gh] where you can [report issues][issues], fork the project and submit pull requests. + +[gh]: https://github.com/fslaborg/RProvider +[issues]: https://github.com/fslaborg/RProvider/issues +[license]: https://github.com/fslaborg/RProvider/blob/master/LICENSE.md +*) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index ebc85876..00000000 --- a/docs/index.md +++ /dev/null @@ -1,71 +0,0 @@ -F# R Type Provider -======= - - -The F# Type Provider is a mechanism that enables smooth interoperability -between F# and [R](http://www.r-project.org/). -The Type Provider discovers R packages that are available -in your R installation and makes them available as .NET namespaces -underneath the parent namespace `RProvider`. - -The Type Provider makes it possible to use -all of R capabilities, from the F# interactive environment. -It enables on-the-fly charting and data analysis using R packages, -with the added benefit of IntelliSense over R, -and compile-time type-checking that the R functions you are using exist. -It allows you to leverage all of .NET libraries, -as well as F# unique capabilities to access and manipulate data -from a wide variety of sources via Type Providers. - -![RProvider demo within an FSharp script](img/vscode.gif) - -The above example is run through F# interactive (`dotnet fsi`). - -### A Quick Demo - -
- -
- -## What are R and F#? - -[F#](http://fsharp.org) is a multi-paradigm language -that supports functional, object and imperative programming, -with the emphasis on functional-first programming. F# runs on the .NET runtime and is a compiled, -statically typed language with a strong type system and type inference. -F# is a general purpose programming language, and is particularly well-suited for scientific/numerical computing. - -[R](http://www.r-project.org/) is an Open Source language for statistical computing. -R offers a wide range of high-quality, community-developed packages, -covering virtually every area of statistics, econometrics or machine learning. -It is also famous for its charting capabilities, making it a great tool -to produce publication-quality graphics. -R is an interpreted, dynamically typed language that is typically used -from its GUI, [RStudio](http://www.rstudio.com/), or command line interactive environment. - -## Using the R Type Provider - -RProvider is distributed as a nuget package. After [setting up the pre-requisites (installing .NET 10+, R 4.5+ and setting the R_HOME environment variable](requirements.html), you can use as follows: - -In an F# script: -```fsharp -#r "nuget:RProvider" - -open RProvider -``` - -To add to a .NET project, from the terminal: -``` -dotnet add package RProvider -``` - -Contributing and copyright --------------------------- - -The project was originally developed by [BlueMountain Capital](https://www.bluemountaincapital.com/). - -The project is hosted on [GitHub][gh] where you can [report issues][issues], fork the project and submit pull requests. - -[gh]: https://github.com/fslaborg/RProvider -[issues]: https://github.com/fslaborg/RProvider/issues -[license]: https://github.com/fslaborg/RProvider/blob/master/LICENSE.md diff --git a/docs/installation.fsx b/docs/installation.fsx index 8540dd31..249c9a07 100644 --- a/docs/installation.fsx +++ b/docs/installation.fsx @@ -24,23 +24,20 @@ Installing RProvider ================================= The R type provider can be used on macOS, Windows, and Linux (for supported OS versions, -see the [.NET 5 OS support matrix](https://github.com/dotnet/core/blob/main/release-notes/5.0/5.0-supported-os.md)). +see the [.NET 10 OS support matrix](https://github.com/dotnet/core/blob/main/release-notes/10.0/10.0-supported-os.md)). There are three **requirements** to use the R type provider: -- [dotnet SDK](https://www.microsoft.com/net/download/core) 5.0 or greater; and -- [R statistical environment](http://cran.r-project.org/). +- [dotnet](https://www.microsoft.com/net/download/core) 10.0 or greater; and +- [R](http://cran.r-project.org/) version 4.5.0 or greater. - A correctly set `R_HOME` environment variable (see below). - You **must** set the `R_HOME` environment variable to the R home directory, - which can usually be identified by running the command 'R RHOME'. -Note. If you require .NET framework / mono support, you should use RProvider 1.2 or earlier. -Support for .NET versions below 5.0 was dropped with RProvider 2.0. +*Note. If you require .NET framework / mono support, you should use RProvider 1.2 or earlier.* Setting the `R_HOME` environment variable ---------------------- The R type provider requires that the R_HOME environment variable is set, so that -it can find the R installation that you wish to use. +it can find the R installation that you wish to use. #### macOS In a Terminal window, execute the following command to add the R_HOME environment @@ -59,12 +56,19 @@ using the command: #### Windows -On Windows, from a command prompt use the following command to set -the R_HOME permanently as a user environment variable, replacing C:\rpath\bin -with your R install location: +On Windows, R_HOME must point to the *root of the R installation*, not the bin directory. +For example, if R is installed in: [lang=cmd] - setx R_HOME "C:\rpath\bin" + C:\Program Files\R\R-4.5.0 + +from a command prompt use the following command to set the R_HOME permanently as a user environment variable: + + [lang=cmd] + setx R_HOME "C:\Program Files\R\R-4.5.0" + +If R_HOME is not set, RProvider will search the standard installation directory (`"C:\Program Files\R\"`) +and automatically select the newest version matching: `R-..`. Testing the R provider ---------------------- @@ -82,38 +86,55 @@ First, create a new file with the extension .fsx (e.g., test.fsx). Second, refer R type provider package from NuGet by adding this line to the start of your file: [lang=fsharp] - #r "nuget: RProvider,2.0.2" + #r "nuget: RProvider,3.0.0" -Third, add your code. In this code, we load RProvider, then load three R packages using -the `open` declarations (graphics, grDevices, and datasets). +Third, add your code. In this code, we load RProvider, then load some R packages using +the `open` declarations. *) open RProvider -open RProvider.graphics -open RProvider.grDevices open RProvider.datasets + +// Pretty-print in F# interactive: +fsi.AddPrinter FSIPrinters.rValue + (** -Now we can run some calculations and create charts. When using R on Mac, the default graphics -device (Quartz) sometimes hangs, but X11 is working without issues, so the following uses X11: +Now, a basic test to make sure it's working correctly. *) -// basic test if RProvider works correctly + R.mean([1;2;3;4]) -// val it : RDotNet.SymbolicExpression = [1] 2.5 +(*** include-it ***) + +(** +We can also do some basic plots. First, we can calculate sin +using the R 'sin' function, and then extract the results from +R to F# for plotting. +*) + +Graphics.svg 7 4 (fun _ -> + [ for x in 0.0 .. 0.1 .. 3.14 -> + R.sin(x).FromR() ] + |> R.plot ) +(*** include-it-raw ***) + +(** +However, it would be cleaner to keep the values in R like so, +as R.sin supports vectors: +*) +Graphics.svg 7 4 (fun _ -> + [ 0.0 .. 0.1 .. 3.14 ] + |> R.sin + |> R.plot ) +(*** include-it-raw ***) -(***do-not-eval***) -// testing graphics -R.x11() +(** +Next, we can plot the nile flow using a standard R example dataset. +*) -(***do-not-eval***) -// Calculate sin using the R 'sin' function -// (converting results to 'float') and plot it -[ for x in 0.0 .. 0.1 .. 3.14 -> - R.sin(x).FromR() ] -|> R.plot +Graphics.svg 7 4 (fun _ -> R.Nile |> R.plot) +(*** include-it-raw ***) -// Plot the data from the standard 'Nile' data set -R.plot(R.Nile) (** Diagnostics and debugging ------------------------- diff --git a/docs/operators.fsx b/docs/operators.fsx index 05f32b2b..572c00c9 100644 --- a/docs/operators.fsx +++ b/docs/operators.fsx @@ -2,7 +2,7 @@ --- category: Core Concepts categoryindex: 3 -index: 3 +index: 4 --- *) diff --git a/docs/packages.md b/docs/packages.md index 4833fe05..a6c53095 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -1,37 +1,35 @@ --- -title: Using R Packages +title: R Packages and Environments category: Guides categoryindex: 4 -index: 2 +index: 3 --- -# How to +# R Packages and Environments -## Printing R values to the console (F# interactive) +RProvider currently accesses the globally installed R, as specified by the `R_HOME` environment variable. A near-term aim is for the provider to access a local `renv`-based package environment, but this requires further development. -Add this line to your script to tell F# interactive how to print out -the values of R objects: +## Accessing packages - [lang=fsharp] - fsi.AddPrinter FSIPrinters.rValue +RProvider discovers the packages installed in your R installation and their functions and lazy data available as packages under the RProvider root namespace. -## Packages +Packages are *not* loaded when their namesapce is opened. If your package requires loading for side-effects, load it using `R.library("somelib")`. -### How do I Load a Package? +## Installing packages -RProvider discovers the packages installed in your R installation and makes them available as packages under the RProvider root namespace. The actual package is lazily loaded the first time you access it. +Currently you need to load up a real R session, then install the package via `install.packages`. You will then need to restart your IDE because the set of installed packages is discovered when RProvider first loads. -### How do I install a new Package? - -Currently you need to load up a real R session, then install the package via install.packages, or the Packages/Install Packages... menu. You will then need to restart Visual Studio because the set of installed packages is cached inside the RProvider. - -#### I have a package installed and it is not showing up -The most likely cause is that RProvider is using a different R installation from the one you updated. When you install R, you get the option to update the registry key `HKEY_LOCAL_MACHINE\SOFTWARE\R-core` to point to the version you are installing. This is what RProvider uses. If you are running in a 32-bit process, RProvider uses `HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R\InstallPath` to determine the path. For 64-bit, it reads `HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R64\InstallPath`. When you install a package in a given version of R, it should be available in both the 32-bit and 64-bit versions. +#### Q. I have a package installed and it is not showing up +The most likely cause is that RProvider is using a different R installation from the one you updated. See [installation](installation.html) for more information. ## Function and Package names + There are a couple of mismatches between allowed identifiers between R and F#: + ### Dots in names + It is pretty common in R to use a dot character in a name, because the character has no special meaning. We remap dots to underscore, and underscore to a double-underscore. So for example, data.frame() becomes R.data_frame(). ### Names that are reserved F# keywords + Some package and function names are reserved words in F#. For these, you will need to quote them using double-backquotes. Typically, the IDE will do this for you. A good example is the base package, which will require an open statement where "base" is double-back-quoted. diff --git a/docs/passing-data.fsx b/docs/passing-data.fsx index f120007d..46c08272 100644 --- a/docs/passing-data.fsx +++ b/docs/passing-data.fsx @@ -2,7 +2,7 @@ --- category: Core Concepts categoryindex: 3 -index: 2 +index: 3 --- *) @@ -22,102 +22,160 @@ index: 2 open RProvider (** -# Passing Data Between F# and R +# Passing Data into R functions from F# -## F# to R: passing data to functions +R functions - as defined in RProvider - do not have strong types for their parameters. How to pass data and functions into R from F# therefore becomes a key consideration. -### Parameter Passing Conventions +## A note on NA and NaN values -R supports various kinds of parameters, which we try to map onto equivalent F# parameter types: +In R, NA is a distinct value on the base atomic types. For example, a factor, numeric, or character vector may contain NA values. In addition, numeric types may be NaN, which is distinct from NA. - * All R formal parameters have names, and you can always pass their values either by name or positionally. If you pass by name, you can skip arguments in your actual argument list. We simply map these onto F# arguments, which you can also pass by name or positionally. +F# does not have a natural representation of NA values within its core types. To mirror R's behaviour, we use option values throughout F#-R data processing in RProvider. You may request non-option values, but if NAs are present any extraction functions will fail with an error to this effect. - * In R, essentially all arguments are optional (even if no default value is specified in the function argument list). It's up to the receiving function to determine whether to error if the value is missing. So we make all arguments optional. +## Passing different types of values - * R functions support ... (varargs/paramarray). We map this onto a .NET ParamArray, which allows an arbitrary number of arguments to be passed. However, there are a couple of kinks with this: +### Existing R values - * R allows named arguments to appear _after_ the ... argument, whereas .NET requires the ParamArray argument to be at the end. Some R functions use this convention because their primary arguments are passed in the ... argument and the named arguments will sometimes be used to modify the behavior of the function. From the RProvider you will to supply values for the positional arguments before you can pass to the ... argument. If you don't want to supply a value to one of these arguments, you can explicitly pass System.Reflection.Missing. +If you pass `RExpr` values (or any semantic R type wrappers, e.g. `Factor`, `DataFrame`, `RComplex`) to any R functions, they will be passed directly to the function without conversion or moving into F# memory space. In effect, F# becomes a typed layer over R. By using semantic R type wrappers, you can orchestrate R computations through the typed view provided by RProvider. +*) + +open RProvider.stats + +R.rnorm(n = 20, mean = 0.2, sd = 0.02) +|> R.mean - * Parameters passed to the R ... argument can also be passed using a name. Those names are accessible to the calling function. Example are list and dataframe construction (R.list, and R.data_frame). To pass arguments this way, you can use the overload of each function that takes an IDictionary, either directly, or using the namedParams function. For example: +(** +### R function parameters - R.data_frame(namedParams [ "A", [|1;2;3|]; "B", [|4;5;6|] ]) +R has some high-level functions (e.g. sapply) that require a function parameter. Although F# has first-class support of functional programming and provides better functionality and syntax for apply-like operations, which often makes it sub-optimal to call apply-like high-level functions in R, the need for parallel computing in R, which is not yet directly supported by F# parallelism to R functions, requires users to pass a function as parameter. Here is an example way to create and pass an R function: +*) +let fun1 = R.eval(R.parse(text="function(i) {mean(rnorm(i))}")) +let nums = R.sapply(R.c(1,2,3), fun1) -### Parameter Types +(** +### F# values -Since all arguments to functions are of type obj, it is not necessarily obvious what you can pass. Ultimately, you will need to know what the underlying function is expecting, but here is a table to help you. When reading this, remember that for most types, R supports only vector types. There are no scalar string, int, bool etc. types. +RProvider contains layers that allow passing F# values to R in either an explicit and implicit way: - - - - - - - - - -
R TypeF#/.NET Type
characterstring or string[]
complexSystem.Numerics.Complex or Complex[]
integerint or int[]
logicalbool or bool[]
numericdouble or double[]
listCall R.list, passing the values as separate arguments
dataframeCall R.data_frame, passing column vectors in a dictionary
+* Explicit: You create an R semantic type from an F# value, which copies it into R memory space. +* Implicit: You specify the F# value as a function parameter to a `R.*` function, causing an implicit conversion. -**NB**: For any input, you can also pass an RExpr instance you received as the result of calling another R function. Doing so it a very efficient way of passing data from one function to the next, since there is no marshalling between .NET and R types in that case. +#### Explicit conversion -### Creating and passing an R function -R has some high-level functions (e.g. sapply) that require a function parameter. Although F# has first-class support of functional programming and provides better functionality and syntax for apply-like operations, which often makes it sub-optimal to call apply-like high-level functions in R, the need for parallel computing in R, which is not yet directly supported by F# parallelism to R functions, requires users to pass a function as parameter. Here is an example way to create and pass an R function: +Using the semantic R types layer, you may pass F# values into appropriate supported R core types. *) -let fun1 = R.eval(R.parse(text="function(i) {mean(rnorm(i))}")) -let nums = R.sapply(R.c(1,2,3),fun1) + +// The namespace containing core types: +open RProvider.Runtime.RTypes + +let vector = Real.Vector.fromFloats [ 1. .. 10. ] + (** -The same usage also applies to parallel apply functions in parallel package. +See [R semantic types](semantic-types.html) for more information. + +#### Implicit conversion (as R function arguments) + +RProvider defines all of the parameters of R functions are obj, meaning that *any* F# type may be passed. Internally, RProvider checks deterministically for a plausable conversion of any F# type specified as a function parameter. + +#### Parameter Types + +Since all arguments to functions are of type obj, it is not necessarily obvious what you can pass. Ultimately, you will need to know what the underlying function is expecting; intellisense and the package's documentation should indicate this. + +Once you have determined *what* R is expecting, you may pass an appropriate F# value. The below table indicates which F# types map to which R types. -## Accessing and using results +| R type | F# type | Notes | +| --- | --- | --- | +| character | string [ + array / list ] [ + option ] | | +| complex | RComplex [ + array / list ] [ + option ] | | +| integer | int [ + array / list ] [ + option ] | | +| logical | bool [ + array / list ] [ + option ] | | +| numeric | float [ + array / list ] [ + option ] | | +| list | - | Call R.list, passing the list elements as the arguments | +| dataframe | - | Call R.data_frame, using a list of named args to indicate the column names | + +Importantly, RProvider accepts single values (e.g. 1), lists ([ 1 ]), arrays ([| 1 |]), and also option values (Some 1), option lists ([ Some 1 ]), and option arrays ([| Some 1 |]). Option values here represent NA in R. + +## Consuming R function results Functions exposed by the RProvider return the erased type `RExpr`. This keeps all return data inside R data structures, so does not impose any data marshalling overhead. If you want to pass the value in as an argument to another R function, you can simply do so. RProvider supports two ways of accessing results: -1. Semantic wrappers. -2. Conversion into .NET values. -### Typed access into key R types +* Explicit: typed access into semantic wrappers and type-specific extraction members / functions. +* Implicit: conversion into F# values using `.FromR`. + +See [working with R expressions](expressions.html) for more information. + +### Explicit typed access + +RProvider includes typed access into R values using the semantic type layer. -The provider includes typed access into R values using a set of semantic type wrappers. +If there are no implicit conversions available, you must either pass the value to another R function for further processing, or use typed access into a relevant semantic type. If there are no supported conversions, you can access the data through the RDotNet object model. RDotNet exposes properties, members and extension members (available only if you open the RDotNet namespace) that allow you to access the underlying data directly. So, for example: *) let res = R.sum([|1;2;3;4|]) -let resInt = res.TryAsVector.Value.AsReal() +res.AsTyped +(*** include-it ***) (** -### Convert the data into a specified .NET type via FromR() - -RProvider adds a generic `GetValue<'T>` extension method to `SymbolicExpression`. This supports conversions from certain R values to specific .NET types. Here are the currently supported conversions: - - - - - - - - - - - - - -
R TypeRequested F#/.NET Type
character (when vector is length 1)string
characterstring[]
complex (when vector is length 1)Complex
complexComplex[]
integer (when vector is length 1)int
integerint[]
logical (when vector is length 1)bool
logicalbool[]
numeric (when vector is length 1)double
numericdouble[]
- -Custom conversions can be supported through [plugins](plugins.html). - -### Convert the data into the default .NET type via .FromR() - -We also expose an extension property called Value that performs a _default_ conversion of a SymbolicExpresion to a .NET type. These are the current conversions: - - - - - - - - -
R TypeF#/.NET Type
characterstring[]
complexComplex[]
integerint[]
logicalbool[]
numericdouble[]
+### Implicit conversion + +Conversion is available to `RExpr` values by using either the `RExpr` module's functions, or using members on the value. + +Without specifying a type, the functions default to obj, so further type checking will be required. +*) + +let pi = R.pi + +RExpr.getValue pi +RExpr.tryGetValue pi + +pi.FromR() +pi.TryFromR() + +(** +It is better to specify the type, so that a strongly typed value may be retrieved: +*) + +RExpr.getValue pi +RExpr.tryGetValue pi + +pi.FromR() +pi.TryFromR() + +(** +Type conversions supported are: + +| R Type | Requested F#/.NET Type | +| --- | --- | +| character (when vector is length 1) | string or string[] (+ option) | +| character | string[] (+ option) | +| complex (when vector is length 1) | RComplex or RComplex[] (+ option) | +| complex | RComplex[] (+ option) | +| integer (when vector is length 1) | int or int[] (+ option) | +| integer | int[] (+ option) | +| logical (when vector is length 1) | bool or bool[] (+ option) | +| logical | bool[] (+ option) | +| numeric (when vector is length 1) | float or float[] (+ option) | +| numeric | float[] (+ option) | + +## Design notes + +R has a very different specification than F#: + +* All R formal parameters have names, and you can always pass their values either by name or positionally. If you pass by name, you can skip arguments in your actual argument list. We simply map these onto F# arguments, which you can also pass by name or positionally. + +* In R, essentially all arguments are optional (even if no default value is specified in the function argument list). It's up to the receiving function to determine whether to error if the value is missing. So we make all arguments optional. + +* R functions support ... (varargs/paramarray). We map this onto a .NET ParamArray, which allows an arbitrary number of arguments to be passed. However, there are a couple of kinks with this: + +* R allows named arguments to appear _after_ the ... argument, whereas .NET requires the ParamArray argument to be at the end. Some R functions use this convention because their primary arguments are passed in the ... argument and the named arguments will sometimes be used to modify the behavior of the function. From the RProvider you will to supply values for the positional arguments before you can pass to the ... argument. If you don't want to supply a value to one of these arguments, you can explicitly pass System.Reflection.Missing. + +* Parameters passed to the R ... argument can also be passed using a name. Those names are accessible to the calling function. Example are list and dataframe construction (R.list, and R.data_frame). To pass arguments this way, you can use the overload of each function that takes an IDictionary, either directly, or using the namedParams function. For example: + + `R.data_frame [ "A", [|1;2;3|]; "B", [|4;5;6|] ]` *) \ No newline at end of file diff --git a/docs/plots.fsx b/docs/plots.fsx new file mode 100644 index 00000000..b52ff19c --- /dev/null +++ b/docs/plots.fsx @@ -0,0 +1,133 @@ +(** +--- +title: Plots and graphics +category: Guides +categoryindex: 4 +index: 2 +--- +*) + +(*** condition: prepare ***) +#nowarn "211" +#r "nuget: RProvider, 0.0.1-local" +(*** condition: fsx ***) +#if FSX +#r "nuget: RProvider,{{package-version}}" +#endif // FSX +(*** condition: ipynb ***) +#if IPYNB +#r "nuget: RProvider,{{package-version}}" +#endif // IPYNB + +(** +# Creating Plots and Graphics + +One of the compelling features of R is its ability to create beautiful plots. +With the R Type Provider, you can use all of R capabilities from F#, +and create simple plots quickly to explore and visualize your data on-the-fly, +as well as generate publication quality graphics that can be exported to virtually any format. + +## Basic R plots + +Basic plots can be found in the graphics package. +Assuming you are using an F# script, +you can reference the required libraries and packages this way: + + [lang=fsharp] + #r "nuget: RProvider,2.0.2" +*) + +open RProvider +open RProvider.graphics + +(** +RProvider includes a `Graphics` module for capturing R plots. +You may also use many (but not all) R graphics devices directly. +See [graphics](graphics.html) for more details. + +The primary helper function is for outputting vector-based non-interactive +plots as svg graphics. Wrap your plot-producing code in a function within `Graphics.svg` as below: +*) + +let widgets = [ 3; 8; 12; 15; 19; 18; 18; 20; ] + +Graphics.svg 7 4 (fun _ -> R.plot widgets) +(*** include-it-raw ***) + +Graphics.svg 7 4 (fun _ -> R.barplot widgets) +(*** include-it-raw ***) + +(** +## Using ggplot2 + +ggplot2 is an R package that expresses the grammar of graphics. +Using RProvider, we can plot F# data in publication-quality plots +using ggplot2. +*) + +open RProvider.ggplot2 +open RProvider.datasets + +let (++) a b = R.ggplot__add (a,b) + +Graphics.svg 7 4 (fun _ -> + R.ggplot(R.mtcars, R.aes(x = "mpg", y = "disp")) ++ + R.geom__point() +) +(*** include-it-raw ***) + + +(** +## Exporting and Saving Charts + +The RProvider Graphics.svg callback (above) may be used to generate +svg graphics. + +Alternatively, you may use R graphics devices directly and manipulate +them through R functions. + +**Note. Some graphics devices are not happy running when R is embedded in another process like RProvider.** +For example, on macOS calling Quartz will crash the process, as it will not run outside of the main thread of a process. +X11 is more stable on macOS. + +An example is shown below of using the PNG device. +*) + +open RProvider.grDevices + +// Open the device and create the file as a png. +// R.bmp, R.jpeg, R.pdf, ... will generate other formats. +R.png(filename = "test.png", height = 200, width = 300, bg = "white") +// Create the chart into the file +R.barplot widgets +// Close the device once the chart is complete +R.dev_off () + + +(** +## R plot arguments + +Named parameters allow you to specify every argument supported by R, +as an list of label and value tuples. + +An example of using named arguments is below. +*) + +open RProvider.Operators + +let sprokets = [ 5.3; 6.5; 1.2; 5.3; 4.; 18.; 15.2; 12.1 ] + +Graphics.svg 7 4 (fun _ -> + R.plot [ + "x" => widgets + "type" => "o" + "col" => "blue" + "ylim" => [0; 25] ] |> ignore + R.lines [ + "x" => sprokets + "type" => "o" + "pch" => 22 + "lty" => 2 + "col" => "red" ] +) +(*** include-it-raw ***) diff --git a/docs/quickstart-statistics.fsx b/docs/quickstart-base.fsx similarity index 81% rename from docs/quickstart-statistics.fsx rename to docs/quickstart-base.fsx index 58029d3a..e927146b 100644 --- a/docs/quickstart-statistics.fsx +++ b/docs/quickstart-base.fsx @@ -1,6 +1,6 @@ (** --- -title: Quickstart: Stats +title: Quickstart: stats category: Getting Started categoryindex: 2 index: 2 @@ -22,21 +22,23 @@ index: 2 (** # Quickstart: Using Statistical Packages -R is a programming language designed for statistics and data mining. -The R community is strong, and created an incredibly rich open source -ecosystem of packages. +A strong R community has contributed over 20,000 packages to CRAN, +R's central package registry. The F# R Type Provider enables you to +use every single one of them from within the F# environment. -The F# R Type Provider enables you to use every single one of them, -from within the F# environment. You can manipulate data using F#, -send it to R for computation, and extract back the results. +Using RRrovider, you can orchestrate R workflows and manipulate R data, +pass in F# values, and extract R values back to F#. + +For this example, we simply demonstrate some basic RProvider concepts +using the built-in `stats` package. ## Example: Linear Regression Let's perform a simple linear regression from the F# interactive, using the R.lm function. -Assuming you installed the R Type Provider in your project from NuGet, -you can reference the required libraries and packages this way: +Once you have referenced RProvider's nuget package in your script, +library, or app, you can reference the required libraries and packages this way: *) open RProvider @@ -51,7 +53,7 @@ Imagine that our true model is Y = 5.0 + 3.0 * X1 - 2.0 * X2 + noise -Let's generate a fake dataset that follows this model: +Let's generate a fake dataset using F# that follows this model: *) // Random number generator @@ -111,6 +113,7 @@ which are both R vectors containg floats: let coefficients = result?coefficients.AsVector().AsReal() let residuals = result?residuals.AsVector().AsReal() + (** We can also produce summary statistics about our model, like R^2, which measures goodness-of-fit - close to 0 @@ -119,15 +122,17 @@ See [R docs for the details on Summary](http://stat.ethz.ch/R-manual/R-patched/l *) let summary = R.summary result + summary?``r.squared``.AsScalar() +(*** include-it ***) (** Finally, we can directly pass results, which is a R expression, to R.plot, to produce some fancy charts describing our model: *) -(***do-not-eval***) -R.plot result +Graphics.svg 8 4 (fun _ -> R.plot result) +(*** include-it-raw ***) (** That's it - while simple, we hope this example illustrate diff --git a/docs/quickstart-charts.fsx b/docs/quickstart-charts.fsx deleted file mode 100644 index 888a2645..00000000 --- a/docs/quickstart-charts.fsx +++ /dev/null @@ -1,168 +0,0 @@ -(** ---- -title: Quickstart: Graphics -category: Getting Started -categoryindex: 2 -index: 3 ---- -*) - -(*** condition: prepare ***) -#nowarn "211" -#r "nuget: RProvider, 0.0.1-local" -(*** condition: fsx ***) -#if FSX -#r "nuget: RProvider,{{package-version}}" -#endif // FSX -(*** condition: ipynb ***) -#if IPYNB -#r "nuget: RProvider,{{package-version}}" -#endif // IPYNB - -(** -# Quickstart: Creating Charts - -One of the compelling features of R is its ability to create beautiful charts. -With the R Type Provider, you can use all of R capabilities from F#, -and create simple charts quickly to explore and visualize your data on-the-fly, -as well as generate publication quality graphics that can be exported to virtually any format. - -## Charts Basics - -Basic charts can be found in the graphics package. -Assuming you are using an F# script, -you can reference the required libraries and packages this way: - - [lang=fsharp] - #r "nuget: RProvider,2.0.2" -*) - -open RProvider -open RProvider.Operators -open RProvider.graphics - -(** -Once the libraries and packages have been loaded, -producing basic charts is as simple as this: -*) - -let widgets = [ 3; 8; 12; 15; 19; 18; 18; 20; ] -let sprockets = [ 5; 4; 6; 7; 12; 9; 5; 6; ] - -(***do-not-eval***) -R.plot(widgets) - -R.plot(widgets, sprockets) - -R.barplot(widgets) - -R.hist(sprockets) - -R.pie(widgets) - -(** -## Exporting and Saving Charts - -Charts can be exported and saved to various formats; -once you have opened the grDevices package, you can save a chart like this: -*) - -(***do-not-eval***) -// Required package to save charts -open RProvider.grDevices - -// Create path to an image testimage.png on the Desktop -let desktop = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Desktop) -let path = desktop + @"\testimage.png" - -// Open the device and create the file as a png. -// R.bmp, R.jpeg, R.pdf, ... will generate other formats. -R.png(filename=path, height=200, width=300, bg="white") -// Create the chart into the file -R.barplot(widgets) -// Close the device once the chart is complete -R.dev_off () - - -(** -## Advanced Charts Options - -The graphic functions exposed by the R Type Provider come in two flavors; -they either have optional named arguments, -followed by a ParamArray for extended arguments, -or they take named parameters, an IDictionary -which contains all the arguments passed to the function. - -### Named Arguments - -Consider for instance the following example: -*) - -(***do-not-eval***) -R.barplot(widgets) -R.title(main="Widgets", xlab="Period", ylab="Quantity") - -(** -R.title has 2 signatures, one of them with optional arguments, -demonstrated above to set the main title as well as the labels for the x and y axis, -ignoring some of the other available options. -You can see another example in the previous section in the R.png call. - -### Named Parameters - -Named parameters allow you to specify every argument supported by R, -as an IDictionary of string, object. -The string is the name of the argument, and the object its value. - -Finding the available arguments for a R function can be tricky; -the full list of arguments can usually be found in the -[R developer documentation](http://stat.ethz.ch/R-manual/R-devel/library/), -navigating in the correct package. For instance, R.plot belongs to -graphics, and can be found -[here](http://stat.ethz.ch/R-manual/R-devel/library/graphics/html/plot.html). - -The easiest way to use that feature is to -leverage the built-in function namedParams, like in this example: -*) - -(***do-not-eval***) -R.plot([ - "x" => widgets - "type" => "o" - "col" => "blue" - "ylim" => [0; 25] ]) - -R.lines([ - "x" => sprockets - "type" => "o" - "pch" => 22 - "lty" => 2 - "col" => "red" ]) - -(** -The first call specifies what to plot (widgets), -what type of line to use, the color, and the scale of the axis. -The second call adds sprockets, specifying lty (the line type), -and pch (the plotting character). - -box is used to reduce all elements to objects, -so that the lists have consistent types. - -A possibly more elegant way to use namedParams is to follow the pattern below: -*) - -(***do-not-eval***) -[ - "x" => widgets - "type" => "o" - "col" => "blue" - "ylim" => [0; 25] ] -|> R.plot - -[ - "x" => sprockets - "type" => "o" - "pch" => 22 - "lty" => 2 - "col" => "red" ] -|> R.lines \ No newline at end of file diff --git a/docs/reading-rdata.fsx b/docs/reading-rdata.fsx index 48dc0034..2f4f8904 100644 --- a/docs/reading-rdata.fsx +++ b/docs/reading-rdata.fsx @@ -6,8 +6,6 @@ index: 1 --- *) -(*** condition: prepare ***) -#nowarn "211" #r "nuget: RProvider, 0.0.1-local" (*** condition: fsx ***) #if FSX @@ -19,39 +17,44 @@ index: 1 #endif // IPYNB (** -Reading and writing RData files +Reading and writing R data files =============================== -When using R, you can save and load data sets as `*.rdata` files. These can be easily -exported and consumed using the R provider too, so if you want to perform part of your -data acquisition, analysis and visualization using F# and another part using R, you -can easily pass the data between F# and R as `*.rdata` files. +When using R, you can save and load data sets as `*.rdata` and `*.rds` files. +RProvider has an RData provider that enables you to work with statically typed +`rdata` files (`.rds` are not supported yet; PRs welcome). -Passing data from R to F# -------------------------- +## Working with .rdata files Let's say that you have some data in R and want to pass them to F#. To do that, you can use the `save` function in R. The following R snippet creates a simple `*.rdata` file containing a couple of symbols from the sample `volcano` data set: - [lang=text] + [lang=R] require(datasets) volcanoList <- unlist(as.list(volcano)) volcanoMean <- mean(volcanoList) symbols <- c("volcano", "volcanoList", "volcanoMean") save(list=symols, file="C:/data/sample.rdata") +### Loading in F# + To import the data on the F# side, you can use the `RData` type provider that is available in the `RProvider` namespace. It takes a static parameter specifying the -path of the file (absolute or relative) and generates a type that exposes all the -saved values as static members: +path of the file and generates a type that exposes all the +saved values as static members. + +Note. You may use absolute or relative paths. If using a relative path, it is +best practice to include `ResolutionFolder = __SOURCE_DIRECTORY__` as below, because +this makes the file resolution happen deterministically relative to the source file, +whether that is an F# script, library, or app. *) open RProvider type Sample = RData<"data/sample.rdata", ResolutionFolder = __SOURCE_DIRECTORY__> let sample = Sample() -// Easily access saved values +// Access saved values directly as F# values sample.volcano sample.volcanoList sample.volcanoMean @@ -59,25 +62,24 @@ sample.volcanoMean (** When accessed, the type provider automatically converts the data from the R format to F# format. In the above example, `volcanoList` is imported as `float[]` and -the `volcanoMean` value is a singleton array. (The provider does not detect that -this is a singleton, so you can get the value using `sample.volcanoMean.[0]`). +the `volcanoMean` value is a `float`, as it is a single-item list in R. For the `sample.volcano` value, the R provider does not have a default conversion and so it is exposed as `SymbolicExpression`. -When you have a number of `*.rdata` files containing data in the same format, you can +When you have multiple `*.rdata` files containing data in the same format, you can pick one of them as a sample (which will be used to determine the fields of the type) and then pass the file name to the constructor of the generated type to load it. For example, if we had files `data/sample_1.rdata` to `data/sample_10.rdata`, we could read them as: *) +(*** do-not-eval ***) let means = [ for i in 1 .. 10 -> let data = Sample(sprintf "data/sample_%d.rdata" i) data.volcanoMean.[0] ] (** -Passing data from F# to R -------------------------- +### Saving from F# If you perform data acquisition in F# and then want to pass the data to R, you can use the standard R functions for saving the `*.rdata` files. The easiest @@ -92,7 +94,7 @@ let sqrs = // Save the squares to an RData file R.assign("volcanoDiffs", sqrs) -R.save(list=[ "volcanoDiffs" ], file="C:/temp/volcano.rdata") +R.save(list=[ "volcanoDiffs" ], file="volcano.rdata") (** It is recommended to use the `list` parameter of the `save` function to specify the names of the symbols that should be saved, rather than saving *all* symbols. The R diff --git a/docs/sample.fsx b/docs/sample.fsx new file mode 100644 index 00000000..b4710db1 --- /dev/null +++ b/docs/sample.fsx @@ -0,0 +1,54 @@ +#r "nuget: RProvider, 0.0.1-local" + +open RProvider +open RProvider.terra +open RProvider.broom + +/// Metres above sea level +[] type masl + +fsi.AddPrinter FSIPrinters.rValue + +/// Load Arctic DEM for Bear Island. +let loadDem () = + let dem = R.rast "/Users/andrewmartin/Tresors/Research Projects/Bear Island/bear-island-arcticdem.tif" + let coords = R.crds dem + let mat = R.as_matrix dem + let matF = + mat.FromR() + |> Array2D.map ((*) 1.) + + // let coordsF = coords.GetValue() + // coordsF + // |> Array2D.map + matF + + mat.Print() + + + dem |> RExpr.slots + + + +open RProvider.stats + +let x = + [| 1 .. 10 |] + |> Array.Parallel.map(fun i -> + (R.sin i).FromR() + ) + +let y = + [| 1 .. 10 |] + |> Array.map(fun i -> + (R.sin i).FromR() + ) + + +// this works +let x2 = [| 0..10 |] |> Array.map (fun i -> R.sin(i).Print()) + +// this fails +let y2 = [| 0..10 |] |> Array.Parallel.map (fun i -> R.sin(i).Print() ) + +R.t_test() \ No newline at end of file diff --git a/docs/semantic-types.fsx b/docs/semantic-types.fsx index f38691c0..31822d5c 100644 --- a/docs/semantic-types.fsx +++ b/docs/semantic-types.fsx @@ -1,9 +1,9 @@ (** --- title: R Semantic Types -category: Guides -categoryindex: 4 -index: 1 +category: Core Concepts +categoryindex: 3 +index: 2 --- *) @@ -20,39 +20,161 @@ index: 1 #endif // IPYNB (** -Operators +Semantic Types =============== -RProvider includes the `RProvider.Operators` module, which contains custom operators that can make working with R easier. Make sure to open it alongside your packages: +RProvider includes a semantic type layer that provides strongly typed wrappers around R values. *) +(*** hide ***) open RProvider open RProvider.Operators -open RProvider.``base`` open RProvider.datasets open RProvider.stats +fsi.AddPrinter FSIPrinters.rValue + +(** +## Numeric + +In R, as in F#, there is a distinction between integer and real (floating-point) numeric values. + +R represents floating point numbers through the 'real' data type. Here, we include semantic types for R's real numbers that include full arithmetic support. Similarly, the integer semantic type includes arithmetic support. + +**Important.** R automatically casts integers to real numbers during any mathematical operations, even if only integers are involved. Our semantic types therefore follow the same casting rules. + +### Scalars + +R does not make a distinction between scalars and vectors; scalars are simply vectors of length one. Here, we have made a distinction. + +To create an integer from an F# value: +*) + +open FSharp.Data.UnitSystems.SI.UnitSymbols + +let i = Runtime.RTypes.Integer.Scalar.fromInt 1 |> Option.get + +(** +Similarly, to create R-based real (floating point) numbers from F# values: +*) + +let x = Runtime.RTypes.Real.Scalar.fromFloat 2.1 |> Option.get +let y = Runtime.RTypes.Real.Scalar.fromFloat 54.9 |> Option.get + +let z = x / y + + (** -# Accessing members / slots +### Vectors + +Vectors may be created within R memory in a similar way to scalars: +*) + +let dist = Runtime.RTypes.Real.Vector.fromFloats [ 1. .. 1. .. 10. ] +let speed = Runtime.RTypes.Real.Vector.fromFloats [ 1. .. 1. .. 10. ] -You can use the dynamic (`?`) operator to access: +// Pass vectors into R functions +let m = R.median dist -* Slots in S4 objects -* Members of list types +// Unit-aware arithmetic +let time = dist / speed -### List: accessing named columns in a dataframe. +(** +### Arithmetic +The int- and float-based R semantic types support F# operators; underneath, these call +R's operators. In effect, you can orchestrate R calculations using the F# typed view +of the values. *) -R.mtcars?mpg +let ex1 = x + x +let ex2 = x * y +let ex3 = ex1 / ex2 (** -### S4 object: access a slot +Of course, RProvider's basic type inference means that we do not know the unit-typing of R's functions, so at present any returned values from R functions are dimensionless, regardless of inputs. + +Note that any operations on integers in R results in real numbers as the return type. + +### Named vectors + +In R, vectors may be named such that each value has a label. RProvider is configured to allow +dotting into the elements of a vector to retrieve a scalar based on either an integer index +or a string name. +*) + +RExpr.typedVectorByName +RExpr.typedVectorByIndex + +(** +## Factors + +Factors are a core R data type and are used to represent categorical variables. In essence, they are a list of classes plus an integer-based vector of indices. You may inspect and extract R factors semantically, as in the below example which again is using the built-in penguin data frame: *) -R.eval "setClass('testclass', representation(foo='character', bar='integer'))" +let species = R.penguins?species.AsFactor() -let test = R.eval "new('testclass', foo='s4', bar=1:4)" +species.Levels.Value +(*** include-it ***) -test?foo +species.Indices + +// Extract the factor to an F# list of strings +species.AsStringVector.Value + +(** +## Heterogeneous Lists (HLists) + +In R, lists may contain values of disparate types. In F#, lists must be homogeneous. + +For example, the let's make a new list: +*) + +let hetL = R.list("apple", [Some "pear"; None], 123.0) + +hetL +(*** include-it ***) + +hetL.Type +(*** include-it ***) + +(** +RProvider includes a semantic type for R's heterogeneous lists. +We can type an R value into the semantic type as follows: +*) + +let hetLSem = hetL.AsList() + +hetLSem.Length +(*** include-it ***) + +hetLSem.[2] +(*** include-it ***) + +(** +If the list has named items, you may also dot in using a string name. + +## Data Frames + +Data frames are a core base type in R. Internally, they are simply lists. + +*) + +let iris = R.iris.AsDataFrame() + +iris.Names +(*** include-it ***) + +(** +Columns may be accessed using `.Column`. The return type is a `Column`, which +is a discriminated union of possible R data types. +*) + +iris.Column "Sepal.Width" +(*** include-it ***) + +(** +We use a DU because the column may be one of many data types, as internally the +data frame is just a heterogeneous list with a class attached. +*) \ No newline at end of file diff --git a/docs/tutorial.fsx b/docs/tutorial.fsx.old similarity index 100% rename from docs/tutorial.fsx rename to docs/tutorial.fsx.old diff --git a/docs/whatwhy.md b/docs/whatwhy.md index 4313e7a8..1be69983 100644 --- a/docs/whatwhy.md +++ b/docs/whatwhy.md @@ -20,9 +20,15 @@ R is an interpreted, dynamically typed language that is typically used from its While there are a number of math/statistical packages available for the .NET platform, none of the approach the power of the packages that are available for R. R also includes versatile packages for visualization which are hard to match on .NET. +The below video is a quick demo (although now dated): + +
+ +
+ ## What is a Type Provider? -F# 3.0 supports a new feature called [Type Providers](http://msdn.microsoft.com/en-us/library/hh156509.aspx) which allow a set of types and members to be determined at compile time (or in the IDE) based on statically known parameters and (optionally) access to some external resource. The primary purpose of Type Providers is to support strongly-typed access to external data sources, without the additional step of code generation, which adds friction to the development process and is sometimes impractical due to the size of the type space. Type Providers can also be used to interoperate with another language or runtime environment, by introspecting on constructs available in that environment during compile time and making equivalent constructs available to F#. +F# since v3 has included a novel feature called [Type Providers](http://msdn.microsoft.com/en-us/library/hh156509.aspx), which allow a set of types and members to be determined at compile time (or in the IDE) based on statically known parameters and (optionally) access to some external resource. The primary purpose of Type Providers is to support strongly-typed access to external data sources, without the additional step of code generation, which adds friction to the development process and is sometimes impractical due to the size of the type space. Type Providers can also be used to interoperate with another language or runtime environment, by introspecting on constructs available in that environment during compile time and making equivalent constructs available to F#. ## Why not just use R directly? diff --git a/global.json b/global.json new file mode 100644 index 00000000..f72210ca --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} \ No newline at end of file diff --git a/src/RProvider.Server/RInteropServer.fs b/src/RProvider.Server/RInteropServer.fs index 2fbcb1d7..15a0e0bc 100644 --- a/src/RProvider.Server/RInteropServer.fs +++ b/src/RProvider.Server/RInteropServer.fs @@ -4,7 +4,6 @@ open System open RProvider.Common open RProvider.Runtime open RProvider.Runtime.RInterop -open RProvider.SymbolicExpressionExtensions /// Event loop (see below) can either perform some work item or stop type internal EventLoopMessage = diff --git a/src/RProvider/Graphics.fs b/src/RProvider/Graphics.fs new file mode 100644 index 00000000..a8b59754 --- /dev/null +++ b/src/RProvider/Graphics.fs @@ -0,0 +1,56 @@ +namespace RProvider + +open RProvider.Runtime +open RProvider.Common + +/// Functions for working with R graphics +module Graphics = + + /// Checks if cairo is available. R's built-in svg depends on cairo. + let private hasCairo globEnv = + try + let res = + RInterop.callFuncByName globEnv "base" "capabilities" + (namedParams ["what", box "cairo"]) [||] + res.FromR() = true + with _ -> false + + /// Capture the output of an R function that uses a graphics device into a string. + /// Width of the SVG to generate + /// Height of the SVG to generate + /// A function that has the side-effect of writing to an active R graphics device. + /// An SVG-formatted XML string. + let svg (width:float) (height:float) (doPlot: unit -> Abstractions.RExpr) = + + let globEnv = RInterop.globalEnvironment() + let tempFileName = System.IO.Path.GetTempFileName() + LogFile.logf "Making SVG in temp file: %s" tempFileName + + try + if RInterop.isPackageInstalled globEnv "svglite" then + LogFile.logf "Using svglite::svglite" + RInterop.callFuncByName globEnv "svglite" "svglite" + (namedParams [ + "file", box tempFileName + "width", box width + "height", box height + ]) [||] + |> ignore + + else if hasCairo globEnv then + LogFile.logf "Using grDevices::svg (cairo available)" + RInterop.callFuncByName globEnv "grDevices" "svg" (namedParams [ + "filename", box tempFileName + "width", box width + "height", box height + ]) [||] |> ignore + + else failwith "Not able to generate SVG. Package svglite not installed and Cairo not available." + + doPlot() |> ignore + RInterop.callFuncByName globEnv "grDevices" "dev.off" [] [||] |> ignore + System.IO.File.ReadAllText tempFileName + + finally + if System.IO.File.Exists tempFileName + then System.IO.File.Delete tempFileName \ No newline at end of file diff --git a/src/RProvider/RExprExtensions.fs b/src/RProvider/RExprExtensions.fs index 6ea4aa78..07ffde0e 100644 --- a/src/RProvider/RExprExtensions.fs +++ b/src/RProvider/RExprExtensions.fs @@ -81,6 +81,7 @@ module RExpr = /// Public API for accessing RExpr, including converting to /// R semantic types, .NET types, and extracting key metadata. +/// [omit] [] module RExprExtensions = diff --git a/src/RProvider/RInterop.fs b/src/RProvider/RInterop.fs index a4532da2..a63ab457 100644 --- a/src/RProvider/RInterop.fs +++ b/src/RProvider/RInterop.fs @@ -125,22 +125,29 @@ module RInterop = let getBindings (packageName: string) = // Get the package environment (not namespace environment) - let pkgEnv = Environment.ofPackage Singletons.engine.Value packageName + let pkgNs = Environment.ofNamespace Singletons.engine.Value packageName - let names = - Evaluate.eval pkgEnv "ls(all.names=TRUE)" + let globalEnv = Environment.globalEnv Singletons.engine.Value + let names = + Call.callFuncByName Convert.toR globalEnv "base" "getNamespaceExports" [] [| pkgNs |] + |> Result.map (Extract.extractStringArray Singletons.engine.Value >> Array.choose id) + |> Result.defaultValue [||] + + let lazyData = + Evaluate.eval globalEnv (sprintf "ls(loadNamespace(\"%s\")$.__NAMESPACE__.$lazydata)" packageName) |> Result.map (Extract.extractStringArray Singletons.engine.Value >> Array.choose id) |> Result.defaultValue [||] names |> Array.choose (fun name -> - match Environment.tryGetValue Singletons.engine.Value pkgEnv name with + match Environment.tryGetValue Singletons.engine.Value pkgNs name with | None -> None | Some sexp -> let forced = Promise.force Singletons.engine.Value sexp let info = bindingInfo forced Some(name, serializeRValue info)) + |> Array.append (lazyData |> Array.map(fun l -> l, serializeRValue RValue.Value)) let globalEnvironment () = Environment.globalEnv Singletons.engine.Value @@ -182,6 +189,18 @@ module RInterop = Call.call Convert.toR packageName funcName serializedRVal namedArgs varArgs |> Result.defaultWith (fun e -> failwithf "Error in function: %s" e) + /// Checks if an R package is installed. + let isPackageInstalled globEnv (pkgName: string) = + try + let res = + callFuncByName globEnv "base" "requireNamespace" + (dict [ + "package", box pkgName + "quietly", box true + ]) [||] + res.FromR() = true + with _ -> false + /// Printing using R's internal print() function. module Printing = @@ -207,10 +226,11 @@ module Printing = callSink env None System.IO.File.ReadAllText temp finally - LogFile.logf "Failed: %s" temp System.IO.File.Delete temp +namespace RProvider + /// Contains helper functions for calling the functions generated by the R provider, /// such as the `namedParams` function for specifying named parameters. /// The module is automatically opened when you open the `RProvider` namespace. diff --git a/src/RProvider/RProvider.fsproj b/src/RProvider/RProvider.fsproj index ee0b3142..97616229 100644 --- a/src/RProvider/RProvider.fsproj +++ b/src/RProvider/RProvider.fsproj @@ -19,6 +19,7 @@ + diff --git a/src/RProvider/SemanticTypes.fs b/src/RProvider/SemanticTypes.fs index 8661aa56..96eedbf6 100644 --- a/src/RProvider/SemanticTypes.fs +++ b/src/RProvider/SemanticTypes.fs @@ -41,7 +41,7 @@ module RTypes = /// Classify a symbolic expression into one of the semantic /// types provided by RProvider. - let classify engine sexp = + let internal classify engine sexp = match sexp with | ActivePatterns.S4Object engine _ -> S4ObjectType | ActivePatterns.DataFrame engine _ -> DataFrameType @@ -85,6 +85,17 @@ module RTypes = | Ok sexp -> tryMake sexp |> Option.get | Error e -> failwith e + let baseOpArray (fn: string) (a: SymbolicExpression array) tryMake = + let rEnv = Environment.globalEnv Singletons.engine.Value + let sexp = Call.callFuncByName passThrough rEnv "base" fn Seq.empty (a |> Array.map(fun a -> a :> obj)) + + match sexp with + | Ok sexp -> tryMake sexp |> Option.get + | Error e -> failwith e + + + /// A basic representation of a vector that does not + /// support numeric operations. module VectorBase = type RVectorBase<'T> = @@ -106,6 +117,38 @@ module RTypes = member this.AsRExpr = this.Sexp |> RExprWrapper.toRProvider + /// A vector of any length may become a generic vector. + let tryFromExpression sexp = + match classify Singletons.engine.Value sexp with + | VectorType + | ScalarType -> { Sexp = sexp } |> Some + | _ -> None + + + /// A basic representation of a scalar value in R, which + /// does not support numeric operators. + module ScalarBase = + + type RScalarBase<'T> = + internal { Sexp: SymbolicExpression } + + member this.AsRExpr = this.Sexp |> RExprWrapper.toRProvider + + /// Creates a scalar from a size-1 vector, or returns None. + let tryFromExpression sexp = + match classify Singletons.engine.Value sexp with + | ScalarType -> { Sexp = sexp } |> Some + | _ -> None + + /// Enforces that a scalar is a vector of a single element, + /// to be used before any operation. + let internal enforceShape num = + if Singletons.engine.Value.invokeInt(fun e -> SymbolicExpression.length e num) = 1 then + num + else + failwith "A scalar R value was mutated and is no longer scalar." + + /// Types for expressing real numbers that are within /// an R environment. Operations are zero-copy within R. module Real = @@ -116,27 +159,19 @@ module RTypes = type RRealScalar<[] 'u> = internal { Sexp: SymbolicExpression } let tryFromExpression (sexp: SymbolicExpression) = - match classify Singletons.engine.Value sexp with - | ScalarType -> + match sexp with + | ActivePatterns.RealVector Singletons.engine.Value _ -> if Singletons.engine.Value.invokeInt(fun e -> SymbolicExpression.length e sexp) = 1 then Some { Sexp = sexp } else None | _ -> None - /// Enforces that a scalar is a vector of a single element, - /// to be used before any operation. - let internal enforceShape (num: RRealScalar<'u>) = - if Singletons.engine.Value.invokeInt(fun e -> SymbolicExpression.length e num.Sexp) = 1 then - num - else - failwith "A scalar R value was mutated and is no longer scalar." - let extractScalarFloat (scalar: RRealScalar<'u>) = - enforceShape scalar - |> fun s -> s.Sexp + ScalarBase.enforceShape scalar.Sexp |> Extract.extractFloatArray Singletons.engine.Value |> Array.head |> Option.map ((*) (LanguagePrimitives.FloatWithMeasure<'u> 1.)) let fromFloat (value: float<'u>) : RRealScalar<'u> option = + let value = value / LanguagePrimitives.FloatWithMeasure<'u> 1 Create.realVector Singletons.engine.Value [| Some value |] |> tryFromExpression type RRealScalar<'u> with @@ -167,27 +202,144 @@ module RTypes = static member (/)(a, b) = RRealScalar.Div a b member this.AsRExpr = this.Sexp |> RExprWrapper.toRProvider - + member this.FromR = lazy(this |> extractScalarFloat) module Vector = type RRealVector<[] 'u> = { Inner: VectorBase.RVectorBase> } - let tryFromExpression sexp = { Inner = { Sexp = sexp } } |> Some + let tryFromExpression sexp = + match sexp with + | ActivePatterns.RealVector Singletons.engine.Value _ -> Some { Inner = { Sexp = sexp } } + | _ -> None + + /// Send an F# sequence of floats to R. + let fromFloats (items: float<'u> seq) : RRealVector<'u> = + Create.realVector Singletons.engine.Value (items |> Seq.map ((*) (LanguagePrimitives.FloatWithMeasure<1/'u> 1.) >> Some)) + |> tryFromExpression + |> Option.get + + /// Send an F# sequence of floats to R, where in F# the floats + /// are option-typed with None representing NA values. + let fromFloatsWithNA items = + Create.realVector Singletons.engine.Value (items |> Seq.map (Option.map <| (*) (LanguagePrimitives.FloatWithMeasure<1/'u> 1.))) + |> tryFromExpression + |> Option.get + + /// Concatenate a sequence of R scalar values into a single + /// R vector. + let fromScalars (items: Scalar.RRealScalar<'u> seq) = + R.baseOpArray "c" (items |> Seq.toArray |> Array.map(fun s -> s.Sexp)) tryFromExpression + + let extract (vector:RRealVector<'u>) = + Extract.extractFloatArray Singletons.engine.Value vector.Inner.Sexp + |> Array.map(Option.map ((*) (LanguagePrimitives.FloatWithMeasure<'u> 1.))) type RRealVector<'u> with - static member Lift(scalar: Scalar.RRealScalar<'u>, vector: RRealVector<'u>) : RRealVector<'u> = + static member Lift(_: Scalar.RRealScalar<'u>, vector: RRealVector<'u>) : RRealVector<'u> = vector + static member Lift(vec1: RRealVector<'u>, _: RRealVector<'u>) = vec1 + + static member Mean(a: RRealVector<'u>) = R.baseOp "mean" a.Inner.Sexp Scalar.tryFromExpression + + static member (+)(a: RRealVector<'u>, b: RRealVector<'u>) : RRealVector<'u> = + R.baseOp2 "+" a.Inner.Sexp b.Inner.Sexp tryFromExpression + static member (-)(a: RRealVector<'u>, b: RRealVector<'u>) : RRealVector<'u> = + R.baseOp2 "-" a.Inner.Sexp b.Inner.Sexp tryFromExpression + static member (*)(a: RRealVector<'u>, b: RRealVector<'v>) : RRealVector<'u 'v> = + R.baseOp2 "*" a.Inner.Sexp b.Inner.Sexp tryFromExpression + static member (/)(a: RRealVector<'u>, b: RRealVector<'v>) : RRealVector<'u/'v> = + R.baseOp2 "/" a.Inner.Sexp b.Inner.Sexp tryFromExpression + + member this.Item(i: int) = this.Inner.[i, Scalar.tryFromExpression] + member this.Item(name: string) = this.Inner.[name, Scalar.tryFromExpression] + member this.Length = R.baseOp "length" this.Inner.Sexp Scalar.tryFromExpression + /// Extract a vector from R to F#, where None is used + /// to represent R's NA values. + member this.FromR = lazy(extract this) + + /// Semantic types for integer vectors and scalars. + /// Supports arithmetic using R functions. + module Integer = + + /// Scalar operations on integers in R. For int-based + /// arithmetic, R will always return real numbers. + module Scalar = + + /// A scalar value currently residing in R's memory space. + type RIntScalar<[] 'u> = internal { Sexp: SymbolicExpression } + + let tryFromExpression (sexp: SymbolicExpression) = + match sexp with + | ActivePatterns.IntegerVector Singletons.engine.Value _ -> + if Singletons.engine.Value.invokeInt(fun e -> SymbolicExpression.length e sexp) = 1 then Some { Sexp = sexp } else None + | _ -> None + + let extractScalar (scalar: RIntScalar<'u>) = + ScalarBase.enforceShape scalar.Sexp + |> Extract.extractIntArray Singletons.engine.Value + |> Array.head + |> Option.map ((*) (LanguagePrimitives.Int32WithMeasure<'u> 1)) + + let fromInt (value: int<'u>) : RIntScalar<'u> option = + let value = value / LanguagePrimitives.Int32WithMeasure<'u> 1 + Create.intVector Singletons.engine.Value [| Some value |] |> tryFromExpression + + let private enforce a = ScalarBase.enforceShape a + + type RIntScalar<'u> with + static member Add (a: RIntScalar<'u>) (b: RIntScalar<'u>) : Real.Scalar.RRealScalar<'u> = + R.baseOp2 "+" (enforce a.Sexp) (enforce b.Sexp) Real.Scalar.tryFromExpression + + static member Sub (a: RIntScalar<'u>) (b: RIntScalar<'u>) : Real.Scalar.RRealScalar<'u> = + R.baseOp2 "-" (enforce a.Sexp) (enforce b.Sexp) Real.Scalar.tryFromExpression + + static member Mul (a: RIntScalar<'u>) (b: RIntScalar<'v>) : Real.Scalar.RRealScalar<'u 'v> = + R.baseOp2 "*" (enforce a.Sexp) (enforce b.Sexp) Real.Scalar.tryFromExpression + + static member Div (a: RIntScalar<'u>) (b: RIntScalar<'v>) : Real.Scalar.RRealScalar<'u / 'v> = + R.baseOp2 "/" (enforce a.Sexp) (enforce b.Sexp) Real.Scalar.tryFromExpression + + static member Log a = R.baseOp "log" (enforce a.Sexp) Real.Scalar.tryFromExpression + static member Exp a = R.baseOp "exp" (enforce a.Sexp) Real.Scalar.tryFromExpression + + static member Scale (a: RIntScalar<'u>) (s: RIntScalar<1>) : RIntScalar<'u> = + R.baseOp2 "*" (enforce a.Sexp) (enforce s.Sexp) tryFromExpression + + static member ToFloat a = extractScalar a + static member FromInt (f: int<'u>) = fromInt f + static member ToReal (a: RIntScalar<'u>) : Real.Scalar.RRealScalar<'u> = R.baseOp "as.double" (enforce a.Sexp) Real.Scalar.tryFromExpression + + static member (+)(a, b) = RIntScalar.Add a b + static member (-)(a, b) = RIntScalar.Sub a b + static member (*)(a, b) = RIntScalar.Mul a b + static member (/)(a, b) = RIntScalar.Div a b + + member this.AsRExpr = this.Sexp |> RExprWrapper.toRProvider + member this.FromR = lazy(this |> extractScalar) + + module Vector = + + type RIntVector<[] 'u> = { Inner: VectorBase.RVectorBase> } + + let tryFromExpression sexp = + match sexp with + | ActivePatterns.IntegerVector Singletons.engine.Value _ -> Some { Inner = { Sexp = sexp } } + | _ -> None + + type RIntVector<'u> with + + static member Lift(scalar: Scalar.RIntScalar<'u>, vector: RIntVector<'u>) : RIntVector<'u> = tryFromExpression scalar.Sexp |> Option.defaultWith(fun _ -> failwith "Could not place scalar into a vector") - static member Lift(vec1: RRealVector<'u>, vec2: RRealVector<'u>) = vec1 + static member Lift(vec1: RIntVector<'u>, vec2: RIntVector<'u>) = vec1 - static member Add (a: RRealVector<'u>) (b: RRealVector<'u>) : RRealVector<'u> = + static member Add (a: RIntVector<'u>) (b: RIntVector<'u>) : RIntVector<'u> = R.baseOp2 "+" a.Inner.Sexp b.Inner.Sexp tryFromExpression - static member Mean(a: RRealVector<'u>) = R.baseOp "mean" a.Inner.Sexp Scalar.tryFromExpression - static member (+)(a, b) = RRealVector.Add a b + static member Mean(a: RIntVector<'u>) = R.baseOp "mean" a.Inner.Sexp Scalar.tryFromExpression + static member (+)(a, b) = RIntVector.Add a b member this.Item(i: int) = this.Inner.[i, Scalar.tryFromExpression] member this.Item(name: string) = this.Inner.[name, Scalar.tryFromExpression] member this.Length = R.baseOp "length" this.Inner.Sexp Scalar.tryFromExpression @@ -195,7 +347,7 @@ module RTypes = type RVector<[] 'u> = | NumericV of Real.Vector.RRealVector<'u> - | IntegerV of VectorBase.RVectorBase + | IntegerV of Integer.Vector.RIntVector<'u> | LogicalV of VectorBase.RVectorBase | CharacterV of VectorBase.RVectorBase | ComplexV of VectorBase.RVectorBase @@ -210,7 +362,7 @@ module RTypes = member internal this.Sexp = match this with | NumericV v -> v.Inner.AsRExpr |> RExprWrapper.toRBridge - | IntegerV v -> v.AsRExpr |> RExprWrapper.toRBridge + | IntegerV v -> v.Inner.AsRExpr |> RExprWrapper.toRBridge | LogicalV v -> v.AsRExpr |> RExprWrapper.toRBridge | CharacterV v -> v.AsRExpr |> RExprWrapper.toRBridge | ComplexV v -> v.AsRExpr |> RExprWrapper.toRBridge @@ -222,8 +374,16 @@ module RTypes = let tryFromExpression sexp = match sexp with | ActivePatterns.RealVector Singletons.engine.Value v -> Real.Vector.tryFromExpression v |> Option.map NumericV - | _ -> - None + | ActivePatterns.IntegerVector Singletons.engine.Value v -> Integer.Vector.tryFromExpression v |> Option.map IntegerV + | ActivePatterns.LogicalVector Singletons.engine.Value _ -> + VectorBase.tryFromExpression sexp |> Option.map LogicalV + | ActivePatterns.CharacterVector Singletons.engine.Value _ -> + VectorBase.tryFromExpression sexp |> Option.map CharacterV + | ActivePatterns.ComplexVector Singletons.engine.Value _ -> + VectorBase.tryFromExpression sexp |> Option.map ComplexV + | ActivePatterns.RawVector Singletons.engine.Value _ -> + VectorBase.tryFromExpression sexp |> Option.map RawV + | _ -> None /// Represents R lists, which may contain elements of any type. module HeterogeneousList = @@ -301,6 +461,7 @@ module RTypes = let tryFromExpression sexp = match classify Singletons.engine.Value sexp with | VectorType -> + RProvider.Common.LogFile.logf "Vector" match sexp with | ActivePatterns.RealVector Singletons.engine.Value v -> Real.Vector.tryFromExpression v |> Option.map NumericColumn | ActivePatterns.IntegerVector Singletons.engine.Value v -> IntegerColumn (RExprWrapper.toRProvider sexp) |> Some @@ -396,10 +557,10 @@ module RTypes = type RScalar<[] 'u> = | NumericS of Real.Scalar.RRealScalar<'u> - | IntegerS of RProvider.Abstractions.RExpr - | LogicalS of RProvider.Abstractions.RExpr - | CharacterS of RProvider.Abstractions.RExpr - | ComplexS of RProvider.Abstractions.RExpr + | IntegerS of Integer.Scalar.RIntScalar<'u> + | LogicalS of ScalarBase.RScalarBase + | CharacterS of ScalarBase.RScalarBase + | ComplexS of ScalarBase.RScalarBase | RawS of RProvider.Abstractions.RExpr with @@ -408,13 +569,33 @@ module RTypes = | NumericS s -> s | _ -> failwith "Expression was not a real number" + member this.AsInt() = + match this with + | IntegerS s -> s + | _ -> failwith "Expression was not an integer" + + member this.AsLogical() = + match this with + | LogicalS s -> s + | _ -> failwith "Expression was not logical (boolean)" + + member this.AsCharacter() = + match this with + | CharacterS s -> s + | _ -> failwith "Expression was not character" + + member this.AsComplex() = + match this with + | ComplexS s -> s + | _ -> failwith "Expression was not complex number" + member internal this.Sexp = match this with | NumericS s -> s.Sexp - | IntegerS s - | LogicalS s - | CharacterS s - | ComplexS s + | IntegerS s -> s.Sexp + | LogicalS s -> s.Sexp + | CharacterS s -> s.Sexp + | ComplexS s -> s.Sexp | RawS s -> s |> RExprWrapper.toRBridge @@ -424,6 +605,15 @@ module RTypes = match sexp with | ActivePatterns.RealVector Singletons.engine.Value v -> Real.Scalar.tryFromExpression v |> Option.map NumericS + | ActivePatterns.IntegerVector Singletons.engine.Value v -> + Integer.Scalar.tryFromExpression v |> Option.map IntegerS + | ActivePatterns.LogicalVector Singletons.engine.Value _ -> + ScalarBase.tryFromExpression sexp |> Option.map LogicalS + | ActivePatterns.CharacterVector Singletons.engine.Value _ -> + ScalarBase.tryFromExpression sexp |> Option.map CharacterS + | ActivePatterns.ComplexVector Singletons.engine.Value _ -> + ScalarBase.tryFromExpression sexp |> Option.map ComplexS + | ActivePatterns.RawVector Singletons.engine.Value _ -> sexp |> RExprWrapper.toRProvider |> RawS |> Some | _ -> failwith "not implemented (Scalar)" diff --git a/src/RProvider/SymbolicExpressionExtensions.fs b/src/RProvider/SymbolicExpressionExtensions.fs index 4548a748..dd20df34 100644 --- a/src/RProvider/SymbolicExpressionExtensions.fs +++ b/src/RProvider/SymbolicExpressionExtensions.fs @@ -1,4 +1,4 @@ -namespace RProvider +namespace RProvider.Runtime open RBridge open RBridge.Extensions @@ -67,9 +67,9 @@ module SymbolicExpression = let column (name: string) (expr: SymbolicExpression) : SymbolicExpression = match expr with | DataFrame Singletons.engine.Value df -> - Runtime.RTypes.DataFrame.tryFromExpression df + RTypes.DataFrame.tryFromExpression df |> Option.defaultWith (fun _ -> failwith "The expression was not a valid R data frame") - |> Runtime.RTypes.DataFrame.getColumn name + |> RTypes.DataFrame.getColumn name |> RTypes.DataFrame.Column.getSexp | _ -> invalidOp "The expression is not an R data frame." @@ -115,18 +115,18 @@ module SymbolicExpression = invalidOp <| sprintf "Unsupported operation on R object (R type: %A)" t let typedVectorByName (name: string) sexp = - match Runtime.RTypes.GenericVector.tryFromExpression sexp with + match RTypes.GenericVector.tryFromExpression sexp with | Some v -> match v with - | Runtime.RTypes.RVector.NumericV v -> v.[name] |> Runtime.RTypes.NumericS + | RTypes.RVector.NumericV v -> v.[name] |> RTypes.NumericS | _ -> failwith "not implemented (typed v by name)" | None -> invalidOp "Expression was not a vector" let typedVectorByIndex (index: int) sexp = - match Runtime.RTypes.GenericVector.tryFromExpression sexp with + match RTypes.GenericVector.tryFromExpression sexp with | Some v -> match v with - | Runtime.RTypes.RVector.NumericV v -> v.[index] |> Runtime.RTypes.NumericS + | RTypes.RVector.NumericV v -> v.[index] |> RTypes.NumericS | _ -> failwith "not implemented (typed v by index)" | None -> invalidOp "Expression was not a vector" @@ -159,21 +159,21 @@ module SymbolicExpressionExtensions = member this.Member(name: string) = SymbolicExpression.getMember name this /// Get the value from the typed vector by name. - member this.ValueOf(name: string) : Runtime.RTypes.RScalar<'u> = SymbolicExpression.typedVectorByName name this + member this.ValueOf(name: string) : RTypes.RScalar<'u> = SymbolicExpression.typedVectorByName name this /// Represents the R value in an appropriate semantic /// R type for further data exploration and analysis, without /// extraction from R memory. member this.TryAsRTyped = SymbolicExpression.tryGetTyped this member this.AsTyped = SymbolicExpression.getTyped this - member this.AsDataFrame = Runtime.RTypes.DataFrame.tryFromExpression this - member this.AsVector = Runtime.RTypes.GenericVector.tryFromExpression this - member this.AsScalar = Runtime.RTypes.GenericScalar.tryFromExpression this - member this.AsFactor = Runtime.RTypes.Factor.tryFromExpression this - member this.AsList = Runtime.RTypes.HeterogeneousList.tryFromExpression this + member this.AsDataFrame = RTypes.DataFrame.tryFromExpression this + member this.AsVector = RTypes.GenericVector.tryFromExpression this + member this.AsScalar = RTypes.GenericScalar.tryFromExpression this + member this.AsFactor = RTypes.Factor.tryFromExpression this + member this.AsList = RTypes.HeterogeneousList.tryFromExpression this /// Get the value from an indexed vector by index. - member this.ValueAt(index: int) : Runtime.RTypes.RScalar<'u> = SymbolicExpression.typedVectorByIndex index this + member this.ValueAt(index: int) : RTypes.RScalar<'u> = SymbolicExpression.typedVectorByIndex index this /// Get the first value of a vector. member this.Head<'a>() = SymbolicExpression.head this diff --git a/tests/RProvider.Tests/RProvider.Tests.fsproj b/tests/RProvider.Tests/RProvider.Tests.fsproj index a83f2168..99ee93c7 100644 --- a/tests/RProvider.Tests/RProvider.Tests.fsproj +++ b/tests/RProvider.Tests/RProvider.Tests.fsproj @@ -8,8 +8,10 @@ + + diff --git a/tests/RProvider.Tests/Runtime/Converters.fs b/tests/RProvider.Tests/Runtime/Converters.fs index e633e45b..04c8f78d 100644 --- a/tests/RProvider.Tests/Runtime/Converters.fs +++ b/tests/RProvider.Tests/Runtime/Converters.fs @@ -1,9 +1,9 @@ module ConverterTests -open RProvider open System open Expecto open FsCheck +open RProvider.Runtime let testVector (xs: 'scalar option []) (t: RBridge.SymbolicExpression.SexpType) clsName = let sexp = SymbolicExpression.ofObj xs diff --git a/tests/RProvider.Tests/Runtime/Graphics.fs b/tests/RProvider.Tests/Runtime/Graphics.fs new file mode 100644 index 00000000..d6bb1bcc --- /dev/null +++ b/tests/RProvider.Tests/Runtime/Graphics.fs @@ -0,0 +1,15 @@ +module GraphicsTests + +open Expecto +open RProvider + +[] +let graphics = + testList "Making graphics" [ + + testCase "Can plot to in-memory svg" <| fun _ -> + let mkSvg () = R.plot [ 1. .. 10. ] + let txt = Graphics.svg 100 100 mkSvg + Expect.stringStarts txt "] diff --git a/tests/RProvider.Tests/TypeProvider/Operators.fs b/tests/RProvider.Tests/TypeProvider/Operators.fs index cfaa12e5..32df3297 100644 --- a/tests/RProvider.Tests/TypeProvider/Operators.fs +++ b/tests/RProvider.Tests/TypeProvider/Operators.fs @@ -2,10 +2,7 @@ module OperatorTests open RProvider open RProvider.Operators -open RProvider.Runtime - open RProvider.datasets -open RProvider.methods open Expecto diff --git a/tests/RProvider.Tests/TypeProvider/Provider.fs b/tests/RProvider.Tests/TypeProvider/Provider.fs index fc245ecd..fc0ce80a 100644 --- a/tests/RProvider.Tests/TypeProvider/Provider.fs +++ b/tests/RProvider.Tests/TypeProvider/Provider.fs @@ -3,8 +3,10 @@ module ProviderTests open System open System.Globalization open RProvider +open RProvider.Operators + open RProvider.datasets -open RProvider.Runtime.Helpers +open RProvider.stats open Expecto @@ -13,7 +15,7 @@ let printing = testList "Printing" [ testCase "Printing of data frame returns string with frame data" <| fun _ -> - let df = namedParams [ "Test", box [| 1; 42; 2 |] ] |> R.data_frame + let df = [ "Test", box [| 1; 42; 2 |] ] |> R.data_frame Expect.stringContains (RExpr.printToString df) "42" "" ] @@ -70,6 +72,20 @@ let locales = ] +[] +let functions = + + testList "Function parameters" [ + + testCase "Can mix named and unnamed arguments" <| fun _ -> + let x = 10. + let summary = R.binom_test(x, 100.0, 0.5, alternative = "two.sided") + let p = summary?``p.value``.FromR() + Expect.floatClose Accuracy.high p 3.06329e-17 "p-value was not correct" + let n = summary?statistic.FromR() + Expect.equal n 10. "number of tries not correct" + ] + [] let main argv = diff --git a/tests/RProvider.Tests/TypeProvider/Semantic.fs b/tests/RProvider.Tests/TypeProvider/Semantic.fs new file mode 100644 index 00000000..df8175fe --- /dev/null +++ b/tests/RProvider.Tests/TypeProvider/Semantic.fs @@ -0,0 +1,25 @@ +module SemanticTests + +open Expecto +open RProvider +open RProvider.datasets + +[] +let interop = + testList "Semantic types - lists" [ + + testCase "Numeric lists can get list items" <| fun _ -> + let l = R.c [1; 2; 3] + Expect.equal l.Type Runtime.RTypes.VectorType "Was not a vector" + Expect.isSome l.TryAsTyped "Could not get semantic view of a vector" + + testCase "Can get typed view of a data frame" <| fun _ -> + Expect.equal R.mtcars.Type Runtime.RTypes.DataFrameType "mtcars was not a data frame" + Expect.isSome R.mtcars.TryAsTyped "Could not get semantic view of a data frame" + + let df = R.mtcars.AsDataFrame() + Expect.contains df.Names (Some "mpg") "Did not infer columns correctly" + + let mpg = df.Column "mpg" + Expect.isTrue mpg.IsNumericColumn "mpg was not numeric" + ] \ No newline at end of file