lainchan / λ

(untitled)

↑ · 2017-10-28 22:12
In F#, how can I generically turn this (probably invalid) JSON: [ { filter : { subject : "foo" }, action : { assign_to : "bar" } }, { filter : { author : "foo" }, action : { status : "bar" } } ] into this (pseudocode): match ticket with {subject="foo"} -> updateticket(assign_to="bar") {author="foo"} -> updateticket(set_status="bar")
The Fin · 2017-10-29 01:56
You gave me an excuse to link this absolutely wonderful F# introduction to parser combinators, which explain how to make a JSON parser: https://fsharpforfunandprofit.com/posts/understanding-parser-combinators-4//
Erik Johansson · 2017-10-29 01:58
>>4601 See also: this talk https://vimeo.com/171704565
Shogun · 2017-10-29 02:04
As for turning the parsed JSON into F# code, it depends. Do you have any number of possible subject/author/whatever fields (which may require code generation, since you want a variable with the same name as your JSON field)? Or just a finite set that you know about?
Turner · 2017-10-29 09:46
>>4601 While parser combinators are a nice topic, I really don't think writing your own JSON parser is a good idea if you're gonna process arbitrary JSON files. There's a lot of subtleties to parsing JSON that a homebrewed parser just isn't going to be able to catch. Also OP, I don't understand what exactly you want to do. Do you want to generate F# code from JSON?
Y.T · 2017-10-29 21:24
>>4603 >>4606 This was just a simplified way to describe what I need, but seeing as I have some genuine responses I'll do my best to better explain the situation. I'm a sysadmin in a Windows-based environment and I use powershell for most of my day-to-day programming needs. I currently have a script running that does ticket triage based on simple rules, and I'd like to convert this to a service for various reasons. I'd like to use F# because I find it interesting and I have the freedom to do so. The current script looks something like this pseudocode: update-ticket { //blah } get-unassignedtickets { //blahh } $tickets = get-unassignedtickets foreach ($ticket in $tickets) { if (($ticket.subject -eq "foo") -or ($ticket.author -eq "bar")) { update-ticket $ticket -assign_to Indian } else if (($ticket.subject -eq "foo") -or ($ticket.author -eq "bar")) { update-ticket $ticket -assign_to ahmed } Except there are many, many if statements. Frankly it's awful, but I'm not a programmer so I don't know a better design pattern for this. Now, my manager has requested that when I convert this to a service, I put the rules in a separate configuration file so that they can be edited by non-technical members of the team. This is where the JSON comes in. I'm not convinced that it's the best choice for a configuration file, but I think it illustrates the point nicely. I have a limited understanding of functional programming, but I think the idiomatic way to approach this is using pattern matching and map(). So what I need is a way to generically generate a pattern-matching function, based on the configuration file, that can be mapped to the list of unassigned tickets. This is what the OP question was about.
push · 2017-10-29 21:35
There's a .NET builtin that does this, no? I remember it being very easy in powershell.
Ivan Horvat · 2017-10-29 21:55
>>4608 So you have a set of conditions, which are checked by only pure functions (the checks are not side-effecting), that always select a single non-pure action to execute? And you want to write an interpreter for a simple DSL expressing these rules that non-technical team members can use?
hash table · 2017-10-29 22:18
>>4610 Yes, that sounds about right. The non-technical team members need to be able to write to a config file something that expresses "if the value of these fields match the values on the ticket object, then perform the following actions". The fields include things like: author, creation date, subject, description, &c. The actions include: assigning the ticket, closing it, editing it, &c. The hard part is turning that into an actual function that can be mapped to the results of get-unassignedtickets. I have no idea how to even approach the problem.
pop · 2017-10-29 23:01
>>4610 So something I'd start with would be the following top-down (semi-pseudocode, I'm more used to Haskell than F# so not everything may be valid code) approach, using user-defined types Filter and Action to describe your possible filters and actions: applyFilter: Ticket -> Filter -> bool applyAction: Ticket -> Action -> () // (ApplyAction is sideeffecting) filterParser: Parser<Filter> actionParser: Parser<Action> caseParser: Parser<Filter * Action> let filterCaseParser = (pchar '{' ) >>. spaces >>. pstring "filter:" >>. spaces >>. filterParser .>> spaces .>> (pchar '}' ) filterActionParser = (pchar '{' ) >>. spaces >>. pstring "action:" >>. spaces >>. actionParser .>> spaces .>> (pchar '}' ) in caseParser = filterCaseParser .>>. (spaces >>. pchar >>. spaces >>. actionCaseParser) DSLParser: Parser<(Filter*Action) list> DSLParser = pchar "{" >>. spaces >>. sepBy1 caseParser (spaces >>. pchar ',' >>. spaces) matchFilter: Ticket -> Filter*Action list -> Action list matchFilter ticket ts = ts |> List.filter (fun (f,a) -> applyFilter ticket f) |> map (fun (f,a) -> a) // impure // Could be modified to return the number of other conditions which would have matched, // if you want employees to know if they use ambiguous logic. executeActionOnTicket: Ticket -> Filter*Action list -> () executeActionOnTicket ticket fas = fas |> matchFilter ticket |> head |> applyAction ticket // If parse succeeds, will execute the first action whose matching filter returned true on the ticket. executeDSLonTicketParser ticket = DSLParser >>= executeActionOnTicket main = ( file stuff, apply runparser with the executing parser on the file text, give error messages if the parse fails, etc) I'll leave you to write the parser for your rules and actions yourself, along with writing the applyFilter and applyAction functions. But this should give you some structure.
Zuse · 2017-10-29 23:03
>>4613 Last parser should be: executeDSLonTicketParser: Ticket -> Parser<()> executeDSLonTicketParser ticket = DSLParser >>= (executeActionOnTicket ticket)
Kay · 2017-10-29 23:04
Wait, F# calls the unit type unit instead of (), doesn't it?
Elliot Alderson · 2017-10-29 23:06
>>4613 Oh, and the second Parser in the let form should be called actionCaseParser, ofc.
byte · 2017-10-29 23:10
>>4613 And DSLparser should be properly terminated with closing braces: DSLParser = pchar "{" >>. spaces >>. ( sepBy1 caseParser (spaces >>. pchar ',' >>. spaces) ) .>> spaces .>> pchar '}'
Neuromancer · 2017-10-29 23:23
>>4612 Regarding the implementation of applyFilter and applyAction, which checks fields by name, and executes methods by name, I believe this may be useful: http://tomasp.net/blog/fsharp-dynamic-lookup.aspx/ The issue is that this can easily cause an error, just like anything that uses reflection. The other alternative is to just define an ADT with a finite number of actions and fields, so that it is a bit more limiting but cannot fail due to employee typos.
Leto Atreides I · 2017-10-29 23:49
>>4608 You don't really need to do all the parsing that the other lainon suggested, I'm sure F# has plenty of good libraries for parsing JSON, and all the more reason to use them if you're actually planning to deploy this in production. You don't want your parser failing because of some JSON edge case that you didn't account for. Functional programming is not really about pattern matching or maps, it's about data, and transformations of data. You have to understand the data before you write a program in a typed functional language like F#. From what I understand, the structure of the JSON files is always { "filter": <something>, "action": <something else> }. You can turn that into a tuple, to pattern match it more easily. type Ticket = (Filter, Action) Then you have to understand what data those Filters and Actions are gonna contain. You're gonna need a union type for this. type Filter = | Subject of string | Author of string | ... And so on, and same for the Action type. Then you can pattern match on those: match ticket with | (Subject "foo", AssignTo "bar") -> ... | (Author "foo", SetStatus "bar") -> ... Of course this only holds up if you have a set number of actions and filters, if you don't then Lain help you.
Sabo Engle · 2017-10-30 00:22
>>4619 But he didn't need to parse actual JSON. He just wanted a JSON-like DSL that the employees could use, which only consists of a list of {filter: {filters...} , action: {action...}} pairs. This language should be regular, unlike JSON which is context free. You WANT to forbid the edge cases here, if they correspond to invalid logic.
nibble · 2017-10-30 02:52
>>4620 >This language should be regular >features balanced braces Choose one.
Erik Johansson · 2017-10-30 06:35
>>4621 You can have nested braces just fine in a regular language. Just not infinitely nested. The DSL that the OP proposes only has three layers of nested braces, and the type of data inside the braces at each level is different.
Hiro Protagonist · 2017-10-30 09:24
>>4620 Of course you want to forbid the edge cases, but you can do that easily in the pattern matching bit rather than in the parsing. For example, if you want to forbid using Subject with SetStatus you can do this: match ticket with ... | (Subject _, SetStatus _) -> raise (Failure "invalid logic")
Project 2501 · 2017-10-30 12:04
>>4620 Anyway, I get that you like F#'s parser combinator and you wanna show it off, but I think you're just gonna confuse the OP by dumping all of that code out, and I don't even think using that is a good way to learn F# nor the ML way of designing a program. OP, you can probably use the parser combinator stuff to parse the config files, but don't use it for error checking in the logic. Parsers are terrible at reporting errors. Also, if the people you're targeting are not technicians, I think you need a better config file format. People are gonna make all sorts of errors like forgetting commas, or double quotes, or unbalanced braces. Have you considered YAML? The same file would look like this. - filter: subject: foo action: assign_to: bar - filter: author: foo action: status: bar
push · 2017-10-30 20:12
OP here. I'm sorry, but either I am too stupid to understand the thorough answers you guys have kindly written out for me, or I am too stupid to properly explain my question to you; whatever the case may be, I'm just not getting this. Thank you sincerely for trying, though. To be clear about a couple of things: The JSON is just an example. I haven't decided on the format for the config file and it isn't relevant to the problem. I don't think I need help with parsing, unless I misunderstand my own question (highly likely). Tickets, filters and actions are actually, behind the scenes, all the same type. A "ticket" has fields that describe the task in our ticketing system - it's ID, subject, author, description etc. There are a finite number of fields and I know them all in advance. A "filter" has all the same fields, but is filled out by the user and is used to match against a ticket. Where the filter matches a ticket, the equivalent "action" is applied. The "action" also has all the same fields, and is passed to the ticketing system's API as an update to ticket that was originally matched against. I hope that makes sense. I mention this only for clarity's sake - I don't think I need help with describing these objects in code, either. What I need help with is the fact that, as far as I can see, I need to somehow generate a function based on the config file provided. Users can choose any combination of fields to put in their filters and actions, so I can't write it out in advance like: match ticket with | (Subject "foo", AssignTo "bar") -> ... | (Author "foo", SetStatus "bar") -> ... I don't know what fields they will want to match on, or what fields they want to update. How do I either generate a pattern matching function, or write something so generic that it can apply to any combination? Thank you again. You've been way more helpful than I ever expected. I'm glad to see the quality of posters here hasn't declined at all.
藤原純一 · 2017-10-30 23:44
>>4637 Ok, I think I understand what you want to do. I'll try to help. From what I understand, you have a list of tickets, and the directives in the config file are supposed to filter that list with a bunch of conditions, returning a smaller list. The resulting list is then passed to the API, which executes the action provided in the config file on it. If so, the "filter" part and the "action" part can be handled separately from each other, and you don't need to generate code or anything like that. >A "filter" has all the same fields, but is filled out by the user and is used to match against a ticket. Make a type for filters, either a union type that you'll return in a list, or a record if you think it'd work better. I think a list would work best since I don't know how many filters could be applied at once, and so you can fold through it like in the example below. I don't know F# so I'm using OCaml syntax here, but it should be easily translatable: let tickets = // the list of all the tickets // the input filters let filters = [ Subject "foo"; Author "bar"; SomethingElse "baz" ] let ticket_matcher filter ticket = match filter with | Subject x -> ticket.subject == x | Author x -> ticket.author == x ... let filtered_list = List.fold_left (fun out filter -> List.filter (ticket_matcher filter) out) tickets filters I'm using currying and folds in this example, feel free to ask if you don't understand something. I'd model the action part as another union type, and then a function that takes both the filtered list of tickets and the action, matches against the action and for each action calls a different API endpoint with those parameters. let perform_action tickets action = match action with | AssignTo x -> API.assignTo x tickets ...
Cypher · 2017-10-30 23:51
>>4637 >>4640 By the way, I've also written out my reasoning because that's what you should do when confronted with a problem in F# (or any language, really). Think about the inputs, their data, the output data, and the relationships between them. Like I said earlier, functional programming (and programming in general) is about the data and the relationships between the data, not about the code.
The Phantom Phreak · 2017-10-31 21:16
>>4640 >>4641 Thank you for these posts. After getting my head around them I was really able to take a step back and look at my problem again. I discovered that my approach was wrong and I had made this much more complicated than it needed to be. I have yet to create a working implementation due to lack of time, but here's how I envision it: type Ticket = { Subject : string; CreatedOn : string; DueDate : string; Id : int; SpentHours : float; } type Filter = { Subject : string; CreatedOn : string; DueDate : string; Id : int?; SpentHours : float?; } type Action = { Subject : string; AssignedToId : int?; IsPrivate : bool?; EstimatedHours : float?; Notes : string; } type Config = Filter * Action let tickets:List<Ticket> = RetrieveUnassignedTickets let configs:List<Config> = [ { Subject = "FOO" },{ Subject = "BAR";Notes = "Changed Subject from FOO to BAR" }; { DueDate = "2018-01-01" },{ DueDate = "2019-01-01";Notes = "Postponed due date by one year" }; { Subject = "^FAO: Indian" },{ AssignedToId = 50;Notes = "Assigned to Indian" }; ] [for ticket in Tickets do for filter,action in Configs -> if Regex.Match(filter.Subject, ticket.Subject) then UpdateTicket(ticket.Id, action) elif String.Equals(filter.DueDate, ticket.DueDate) then UpdateTicket(ticket.Id, action) ...] Thanks again to everyone here who helped me out. It's been a great learning experience for me. Apologies to >>4613 - I wish I was smart enough to understand this post because you clearly put a lot of effort into it for my sake.
Lain Iwakura · 2017-11-01 05:20
>>4657 Hey OP Why the fuarrrk are you using F#? Not to be mean like genuinely why the fuarrrk is this? what is this? what is this?
Tyrell Wellick · 2017-11-01 06:33
>>4608 >>4658 >I'd like to use F# because I find it interesting and I have the freedom to do so.
Mario Rossi · 2017-11-01 09:21
>>4658 >OP has to write a program >F# is a general purpose programming langauge What's your point?
Trinity · 2017-11-01 13:22
>>4661 >>4660 I just want to know why I dont understand the defensiveness here. It's not exactly a common language to use so it's a fair question. What made you choose this instead of OCaml or something more mainstream?
Netrunner · 2017-11-01 13:39
>>4664 My employers enforce a strict Microsoft-only policy, unfortunately. My options are limited.